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.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
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 deviceKeyDataSource: FirestoreDeviceKeyDataSource,
    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 using sealed:v1: format (schemaVersion 3).
     * The private key is created on first call if it does not exist yet.
     */
    suspend fun saveAnswer(
        coupleId: String,
        questionId: String,
        userId: String,
        answer: LocalAnswer,
        date: String = todayLocalDateString()
    ): Unit = saveAnswerSealed(coupleId, questionId, userId, answer, date)

    private suspend fun saveAnswerSealed(
        coupleId: String,
        questionId: String,
        userId: String,
        answer: LocalAnswer,
        date: String
    ) {
        ensureUserPublicKeyPublished(userId)
        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("daily_${date}_${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
        )

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

    /** Updates the answerKeyReleased flag after the one-time key is sent to the partner. */
    suspend fun markAnswerKeyReleased(coupleId: String, date: String, userId: String): Unit =
        suspendCancellableCoroutine { cont ->
            answerRef(coupleId, date, userId)
                // updatedAt is stored as epoch millis everywhere else; a serverTimestamp here made
                // it a Firestore Timestamp, which crashed toLocalAnswer's getLong("updatedAt") when
                // the partner read this answer during reveal. Keep it a Long.
                .update(mapOf("answerKeyReleased" to true, "updatedAt" to System.currentTimeMillis()))
                .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
                }
                cont.resume(snap.toLocalAnswer())
            }
            .addOnFailureListener { cont.resumeWithException(it) }
    }

    /**
     * Live view of a partner's answer doc — emits whenever they submit or release their
     * reveal key. Lets the reveal screen complete the client-side decryption in real time
     * instead of waiting for a manual refresh. Errors are swallowed (the screen keeps its
     * current state) so the flow never crashes the collector.
     */
    fun observeAnswerForUser(
        coupleId: String,
        userId: String,
        date: String = todayLocalDateString()
    ): Flow<LocalAnswer?> = callbackFlow {
        val reg = answerRef(coupleId, date, userId).addSnapshotListener { snap, err ->
            if (err != null) return@addSnapshotListener
            trySend(snap?.takeIf { it.exists() }?.toLocalAnswer())
        }
        awaitClose { reg.remove() }
    }

    /**
     * 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) }
        }

    private fun com.google.firebase.firestore.DocumentSnapshot.toLocalAnswer(): LocalAnswer {
        // All answers are sealed (schemaVersion 3): content lives in encryptedPayload and is
        // decrypted later by SealedRevealManager. Nothing is ever stored in plaintext.
        return LocalAnswer(
            questionId = getString("questionId") ?: "",
            questionText = "",
            category = "",
            answerType = getString("answerType") ?: "written",
            createdAt = longOrTimestampMillis("createdAt"),
            updatedAt = longOrTimestampMillis("updatedAt"),
            isRevealed = getBoolean("isRevealed") ?: false,
            schemaVersion = 3,
            isSealed = getBoolean("answerKeyReleased") != true,
            encryptedPayload = getString("encryptedPayload"),
            answerDate = getString("answerDate") ?: ""
        )
    }

    /**
     * Reads an epoch-millis field that may have been written as a Long, a Firestore Timestamp,
     * or a Date (older docs / serverTimestamp writes). getLong() throws on a non-number field,
     * which previously crashed the reveal — this tolerates every shape.
     */
    private fun com.google.firebase.firestore.DocumentSnapshot.longOrTimestampMillis(field: String): Long =
        when (val v = get(field)) {
            is Number -> v.toLong()
            is com.google.firebase.Timestamp -> v.toDate().time
            is java.util.Date -> v.time
            else -> System.currentTimeMillis()
        }

    /**
     * Saves a "Couple Lore" entry to Firestore.
     * Path: couples/{coupleId}/lore/{questionId}
     */
    suspend fun saveLoreEntry(
        coupleId: String,
        questionId: String,
        questionText: String,
        ownAnswer: String,
        partnerAnswer: String?,
        modeTag: String?,
        date: String
    ): Unit = suspendCancellableCoroutine { cont ->
        val aead = encryptionManager.requireAead(coupleId)
        val doc = mapOf(
            "questionId" to questionId,
            "questionText" to questionText,
            "ownAnswer" to fieldEncryptor.encrypt(ownAnswer, aead, coupleId),
            "partnerAnswer" to fieldEncryptor.encryptNullable(partnerAnswer, aead, coupleId),
            "modeTag" to modeTag,
            "date" to date,
            "schemaVersion" to 2,
            "savedAt" to com.google.firebase.Timestamp.now()
        )
        db.collection(FirestoreCollections.COUPLES)
            .document(coupleId)
            .collection("lore")
            .document(questionId)
            .set(doc)
            .addOnSuccessListener { cont.resume(Unit) }
            .addOnFailureListener { cont.resumeWithException(it) }
    }

    private suspend fun ensureUserPublicKeyPublished(userId: String) {
        if (deviceKeyDataSource.getPublicKey(userId) != null) return
        val privateKey = userKeyManager.getOrCreatePrivateKey()
        deviceKeyDataSource.publishPublicKey(userId, userKeyManager.publicKeyB64(privateKey))
    }

    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)
        }
    }
}
