package app.closer.data.remote

import app.closer.crypto.CoupleEncryptionManager
import app.closer.crypto.FieldEncryptor
import com.google.firebase.firestore.DocumentSnapshot
import com.google.firebase.firestore.FirebaseFirestore
import com.google.firebase.firestore.SetOptions
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.tasks.await
import org.json.JSONArray
import org.json.JSONObject
import javax.inject.Inject
import javax.inject.Singleton

/** One prompt in a wheel session, denormalized so the reveal needs no question reload. */
data class WheelQuestionRef(val id: String = "", val text: String = "")

/** One partner's answer to a prompt, pre-rendered to a display string at submit time. */
data class WheelAnswerEntry(val questionId: String = "", val display: String = "")

/** The full shared state of a wheel session's reveal. */
data class WheelRevealDoc(
    val categoryName: String = "",
    val questions: List<WheelQuestionRef> = emptyList(),
    val answersByUser: Map<String, List<WheelAnswerEntry>> = emptyMap()
)

/**
 * Stores both partners' wheel answers for the async reveal at
 * `couples/{coupleId}/wheel/{sessionId}`. The session (shared question set + the
 * one-active-game lock) is owned by [app.closer.domain.usecase.GameSessionManager];
 * this carries the answers so each partner plays the same spun set on their own
 * device and the result reveals once both have submitted.
 */
@Singleton
class FirestoreWheelAnswerDataSource @Inject constructor(
    private val db: FirebaseFirestore,
    private val encryptionManager: CoupleEncryptionManager,
    private val fieldEncryptor: FieldEncryptor
) {
    private fun doc(coupleId: String, sessionId: String) =
        db.collection(FirestoreCollections.COUPLES)
            .document(coupleId)
            .collection(FirestoreCollections.Couples.WHEEL)
            .document(sessionId)

    /** Persist this user's answers (display strings aligned to the question set). */
    suspend fun submitAnswers(
        coupleId: String,
        sessionId: String,
        userId: String,
        categoryName: String,
        questions: List<WheelQuestionRef>,
        answers: List<WheelAnswerEntry>
    ) {
        val aead = encryptionManager.requireAead(coupleId)
        val answerJson = JSONArray(answers.map {
            JSONObject().apply {
                put("questionId", it.questionId)
                put("display", it.display)
            }
        }.toString())
        val data = mapOf(
            "categoryName" to categoryName,
            "questions" to questions.map { mapOf("id" to it.id, "text" to it.text) },
            "answers" to mapOf(userId to fieldEncryptor.encrypt(answerJson.toString(), aead, coupleId))
        )
        doc(coupleId, sessionId).set(data, SetOptions.merge()).await()
    }

    /** One-shot read — used to detect whether this user has already answered. */
    suspend fun getDoc(coupleId: String, sessionId: String): WheelRevealDoc? =
        runCatching { parse(doc(coupleId, sessionId).get().await(), coupleId) }.getOrNull()

    /** Live view of both partners' answers; emits whenever either side submits. */
    fun observe(coupleId: String, sessionId: String): Flow<WheelRevealDoc> = callbackFlow {
        val reg = doc(coupleId, sessionId).addSnapshotListener { snap, err ->
            if (err != null) { close(err); return@addSnapshotListener }
            if (snap == null) return@addSnapshotListener
            trySend(parse(snap, coupleId))
        }
        awaitClose { reg.remove() }
    }

    private fun parse(snap: DocumentSnapshot, coupleId: String): WheelRevealDoc {
        val aead = encryptionManager.aeadFor(coupleId)
        val questions = (snap.get("questions") as? List<*>).orEmpty().mapNotNull { item ->
            (item as? Map<*, *>)?.let {
                WheelQuestionRef(
                    id = it["id"] as? String ?: "",
                    text = it["text"] as? String ?: ""
                )
            }
        }
        @Suppress("UNCHECKED_CAST")
        val rawAnswers = snap.get("answers") as? Map<String, *>
        val answersByUser = rawAnswers.orEmpty().mapValues { (_, value) ->
            when (value) {
                is String -> {
                    // An encrypted blob that can't be decrypted here means the partner DID
                    // answer but this device lacks the key — surface a locked row per prompt
                    // instead of silently showing nothing (which reads as "didn't answer").
                    val json = fieldEncryptor.decrypt(value, aead, coupleId)
                        ?: return@mapValues questions.map {
                            WheelAnswerEntry(it.id, FieldEncryptor.LOCKED_PLACEHOLDER)
                        }
                    runCatching {
                        val array = JSONArray(json)
                        (0 until array.length()).map { index ->
                            val item = array.getJSONObject(index)
                            WheelAnswerEntry(
                                questionId = item.optString("questionId"),
                                display = item.optString("display")
                            )
                        }
                    }.getOrDefault(emptyList())
                }
                else -> emptyList()
            }
        }
        return WheelRevealDoc(
            categoryName = snap.getString("categoryName") ?: "",
            questions = questions,
            answersByUser = answersByUser
        )
    }
}
