package app.closer.data.remote

import app.closer.crypto.AnswerCommitment
import app.closer.crypto.CoupleEncryptionManager
import app.closer.crypto.FieldEncryptor
import app.closer.crypto.PendingAnswerKeyStore
import app.closer.crypto.SealedAnswerEncryptor
import app.closer.crypto.UserKeyManager
import app.closer.domain.model.LocalAnswer
import com.google.firebase.firestore.FirebaseFirestore
import kotlinx.coroutines.suspendCancellableCoroutine
import org.json.JSONArray
import java.time.Clock
import java.time.Instant
import java.time.LocalDate
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException

/**
 * Syncs daily-question answers to Firestore under the couple's daily question doc.
 *
 * Path: couples/{coupleId}/daily_question/{date}/answers/{userId}
 *
 * Only the authenticated user may create/update their own answer document;
 * both partners may read either answer. Firestore rules enforce this.
 */
@Singleton
class FirestoreAnswerDataSource @Inject constructor(
    private val db: FirebaseFirestore,
    private val encryptionManager: CoupleEncryptionManager,
    private val fieldEncryptor: FieldEncryptor,
    private val userKeyManager: UserKeyManager,
    private val sealedAnswerEncryptor: SealedAnswerEncryptor,
    private val pendingAnswerKeyStore: PendingAnswerKeyStore,
    private val answerCommitment: AnswerCommitment
) {

    private fun dailyQuestionRef(coupleId: String, date: String) =
        db.collection(FirestoreCollections.COUPLES)
            .document(coupleId)
            .collection(FirestoreCollections.Couples.DAILY_QUESTION)
            .document(date)

    private fun answerRef(coupleId: String, date: String, userId: String) =
        dailyQuestionRef(coupleId, date).collection(FirestoreCollections.DailyQuestion.ANSWERS).document(userId)

    /**
     * Persists the user's answer to Firestore. Uses sealed:v1: format (schemaVersion 3)
     * when the user has an ECIES keypair, giving partner-proof privacy. Falls back to
     * enc:v1: format (schemaVersion 2) if no keypair is present yet.
     */
    suspend fun saveAnswer(
        coupleId: String,
        questionId: String,
        userId: String,
        answer: LocalAnswer,
        date: String = todayLocalDateString()
    ): Unit {
        val privateKey = userKeyManager.loadPrivateKey()
        if (privateKey != null) {
            saveAnswerSealed(coupleId, questionId, userId, answer, date)
        } else {
            saveAnswerEncrypted(coupleId, questionId, userId, answer, date)
        }
    }

    private suspend fun saveAnswerSealed(
        coupleId: String,
        questionId: String,
        userId: String,
        answer: LocalAnswer,
        date: String
    ): Unit = suspendCancellableCoroutine { cont ->
        val oneTimeKey = sealedAnswerEncryptor.generateOneTimeKey()
        val payload = SealedAnswerEncryptor.AnswerPayload(
            writtenText = answer.writtenText,
            selectedOptionIds = answer.selectedOptionIds,
            scaleValue = answer.scaleValue
        )
        val encryptedPayload = sealedAnswerEncryptor.seal(payload, oneTimeKey, coupleId, questionId, userId)
        val commitment = answerCommitment.compute(
            coupleId, questionId, userId,
            answer.writtenText, answer.selectedOptionIds, answer.scaleValue
        )

        pendingAnswerKeyStore.store(questionId, oneTimeKey)

        val data = mapOf(
            "userId" to userId,
            "questionId" to questionId,
            "answerType" to answer.answerType,
            "encryptedPayload" to encryptedPayload,
            "commitmentHash" to commitment,
            "schemaVersion" to 3,
            "answerKeyReleased" to false,
            "answerDate" to date,
            "createdAt" to answer.createdAt,
            "updatedAt" to answer.updatedAt,
            "isRevealed" to answer.isRevealed
        )

        answerRef(coupleId, date, userId)
            .set(data)
            .addOnSuccessListener { cont.resume(Unit) }
            .addOnFailureListener { cont.resumeWithException(it) }
    }

    private suspend fun saveAnswerEncrypted(
        coupleId: String,
        questionId: String,
        userId: String,
        answer: LocalAnswer,
        date: String
    ): Unit = suspendCancellableCoroutine { cont ->
        val aead = encryptionManager.requireAead(coupleId)
        val data = mapOf(
            "userId" to userId,
            "questionId" to questionId,
            "answerType" to answer.answerType,
            "writtenText" to fieldEncryptor.encryptNullable(answer.writtenText, aead, coupleId),
            "selectedOptionIds" to if (answer.selectedOptionIds.isNotEmpty())
                listOf(fieldEncryptor.encrypt(JSONArray(answer.selectedOptionIds).toString(), aead, coupleId))
            else answer.selectedOptionIds,
            "scaleValue" to if (answer.scaleValue != null)
                fieldEncryptor.encrypt(answer.scaleValue.toString(), aead, coupleId)
            else answer.scaleValue,
            "schemaVersion" to 2,
            "answerDate" to date,
            "createdAt" to answer.createdAt,
            "updatedAt" to answer.updatedAt,
            "isRevealed" to answer.isRevealed
        )

        answerRef(coupleId, date, userId)
            .set(data)
            .addOnSuccessListener { cont.resume(Unit) }
            .addOnFailureListener { cont.resumeWithException(it) }
    }

    /**
     * Fetches a partner's answer for the current local date.
     */
    suspend fun getAnswerForUser(
        coupleId: String,
        userId: String,
        date: String = todayLocalDateString()
    ): LocalAnswer? = suspendCancellableCoroutine { cont ->
        answerRef(coupleId, date, userId)
            .get()
            .addOnSuccessListener { snap ->
                if (!snap.exists()) {
                    cont.resume(null)
                    return@addOnSuccessListener
                }
                val aead = encryptionManager.aeadFor(coupleId)
                cont.resume(snap.toLocalAnswer(aead, coupleId))
            }
            .addOnFailureListener { cont.resumeWithException(it) }
    }

    /**
     * Reads the couple-scoped daily question assignment for today.
     */
    suspend fun getDailyQuestionAssignment(coupleId: String, date: String = todayLocalDateString()): DailyQuestionAssignment? =
        suspendCancellableCoroutine { cont ->
            dailyQuestionRef(coupleId, date)
                .get()
                .addOnSuccessListener { snap ->
                    if (!snap.exists()) {
                        cont.resume(null)
                        return@addOnSuccessListener
                    }
                    cont.resume(
                        DailyQuestionAssignment(
                            questionId = snap.getString("questionId") ?: "",
                            date = snap.getString("date") ?: date,
                            assignedAt = snap.getTimestamp("assignedAt"),
                            expiresAt = snap.getTimestamp("expiresAt")
                        )
                    )
                }
                .addOnFailureListener { cont.resumeWithException(it) }
        }

    /**
     * Calls the cloud function to assign a daily question for the couple immediately.
     * Used when a couple is newly created and has no assignment yet.
     */
    suspend fun requestDailyQuestionAssignment(coupleId: String, date: String = todayLocalDateString()): Unit =
        suspendCancellableCoroutine { cont ->
            val functions = com.google.firebase.functions.FirebaseFunctions.getInstance()
            functions
                .getHttpsCallable("assignDailyQuestionCallable")
                .call(mapOf("coupleId" to coupleId, "date" to date))
                .addOnSuccessListener { cont.resume(Unit) }
                .addOnFailureListener { cont.resumeWithException(it) }
        }

    @Suppress("UNCHECKED_CAST")
    private fun com.google.firebase.firestore.DocumentSnapshot.toLocalAnswer(
        aead: com.google.crypto.tink.Aead?,
        coupleId: String
    ): LocalAnswer {
        val schemaVersion = getLong("schemaVersion")?.toInt() ?: 2

        // schemaVersion 3: sealed:v1: — content is in encryptedPayload, not top-level fields.
        // The calling code (reveal flow) is responsible for decrypting via SealedRevealManager.
        if (schemaVersion == 3) {
            return LocalAnswer(
                questionId = getString("questionId") ?: "",
                questionText = "",
                category = "",
                answerType = getString("answerType") ?: "written",
                createdAt = getLong("createdAt") ?: System.currentTimeMillis(),
                updatedAt = getLong("updatedAt") ?: System.currentTimeMillis(),
                isRevealed = getBoolean("isRevealed") ?: false,
                schemaVersion = 3,
                isSealed = getBoolean("answerKeyReleased") != true,
                encryptedPayload = getString("encryptedPayload"),
                answerDate = getString("answerDate") ?: ""
            )
        }

        // schemaVersion 2: enc:v1: — decrypt with couple AEAD.
        val rawIds = get("selectedOptionIds") as? List<String> ?: emptyList()
        val selectedOptionIds = if (rawIds.size == 1 && fieldEncryptor.isEncrypted(rawIds[0])) {
            val decrypted = fieldEncryptor.decrypt(rawIds[0], aead, coupleId)
            if (decrypted != null) runCatching {
                val arr = org.json.JSONArray(decrypted)
                (0 until arr.length()).map { arr.getString(it) }
            }.getOrDefault(emptyList()) else emptyList()
        } else rawIds

        val rawScale = get("scaleValue")
        val scaleValue: Int? = when {
            rawScale == null -> null
            rawScale is String && fieldEncryptor.isEncrypted(rawScale) ->
                fieldEncryptor.decrypt(rawScale, aead, coupleId)?.toIntOrNull()
            rawScale is Long -> rawScale.toInt()
            rawScale is Int -> rawScale
            else -> null
        }

        return LocalAnswer(
            questionId = getString("questionId") ?: "",
            questionText = "",
            category = "",
            answerType = getString("answerType") ?: "written",
            writtenText = fieldEncryptor.decrypt(getString("writtenText"), aead, coupleId),
            selectedOptionIds = selectedOptionIds,
            selectedOptionTexts = emptyList(),
            scaleValue = scaleValue,
            createdAt = getLong("createdAt") ?: System.currentTimeMillis(),
            updatedAt = getLong("updatedAt") ?: System.currentTimeMillis(),
            isRevealed = getBoolean("isRevealed") ?: false,
            schemaVersion = schemaVersion,
            answerDate = getString("answerDate") ?: ""
        )
    }

    data class DailyQuestionAssignment(
        val questionId: String,
        val date: String,
        val assignedAt: com.google.firebase.Timestamp?,
        val expiresAt: com.google.firebase.Timestamp?
    )

    companion object {
        private val dateKeyFormatter: DateTimeFormatter = DateTimeFormatter.ISO_LOCAL_DATE

        /**
         * Daily-question document IDs follow the user's local calendar day.
         *
         * Firestore timestamps remain absolute instants, but the `daily_question/{date}`
         * key should match what the user experiences as "today" on their device.
         */
        fun todayLocalDateString(clock: Clock = Clock.systemDefaultZone()): String {
            return LocalDate.now(clock).format(dateKeyFormatter)
        }

        fun localDateString(instant: Instant, zoneId: ZoneId = ZoneId.systemDefault()): String {
            return instant.atZone(zoneId).toLocalDate().format(dateKeyFormatter)
        }
    }
}
