package app.closer.data.remote

import app.closer.crypto.RecoveryKeyManager
import app.closer.crypto.EncryptionVersion
import app.closer.domain.model.Couple
import com.google.firebase.Timestamp
import com.google.firebase.firestore.DocumentSnapshot
import com.google.firebase.firestore.FirebaseFirestore
import com.google.firebase.firestore.SetOptions
import com.google.firebase.functions.FirebaseFunctions
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.tasks.await
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException

@Singleton
class FirestoreCoupleDataSource @Inject constructor(
    private val db: FirebaseFirestore,
    private val functions: FirebaseFunctions
) {
    private fun coupleRef(coupleId: String) = db.collection(FirestoreCollections.COUPLES).document(coupleId)
    private fun userRef(uid: String) = db.collection(FirestoreCollections.USERS).document(uid)

    /** [coupleId] is pre-generated by the repository so the keyset can be stored locally first. */
    suspend fun createCouple(
        coupleId: String,
        inviterUserId: String,
        acceptorUserId: String,
        inviteCode: String,
        wrappedKey: RecoveryKeyManager.WrappedKey
    ): String {
        val now = System.currentTimeMillis()
        createCoupleDoc(coupleId, inviterUserId, acceptorUserId, inviteCode, now, wrappedKey)
        updateUserCoupleId(inviterUserId, coupleId)
        updateUserCoupleId(acceptorUserId, coupleId)
        return coupleId
    }

    private suspend fun createCoupleDoc(
        coupleId: String,
        inviterUserId: String,
        acceptorUserId: String,
        inviteCode: String,
        now: Long,
        wrappedKey: RecoveryKeyManager.WrappedKey
    ): Unit = suspendCancellableCoroutine { cont ->
        val data = mutableMapOf<String, Any>(
            "id" to coupleId,
            "userIds" to listOf(inviterUserId, acceptorUserId),
            "inviteCode" to inviteCode,
            "createdAt" to now,
            "streakCount" to 0
        )
        data["encryptionVersion"] = EncryptionVersion.STRICT
        data["wrappedCoupleKey"] = wrappedKey.cipherB64
        data["kdfSalt"] = wrappedKey.saltB64
        data["kdfParams"] = wrappedKey.params
        coupleRef(coupleId).set(data)
            .addOnSuccessListener { cont.resume(Unit) }
            .addOnFailureListener { cont.resumeWithException(it) }
    }

    suspend fun updateWrappedKey(coupleId: String, wrappedKey: RecoveryKeyManager.WrappedKey): Unit =
        suspendCancellableCoroutine { cont ->
            coupleRef(coupleId).set(
                mapOf(
                    "wrappedCoupleKey" to wrappedKey.cipherB64,
                    "kdfSalt" to wrappedKey.saltB64,
                    "kdfParams" to wrappedKey.params
                ),
                SetOptions.merge()
            )
                .addOnSuccessListener { cont.resume(Unit) }
                .addOnFailureListener { cont.resumeWithException(it) }
        }

    private suspend fun updateUserCoupleId(uid: String, coupleId: String): Unit =
        suspendCancellableCoroutine { cont ->
            userRef(uid).set(
                mapOf("coupleId" to coupleId),
                SetOptions.merge()
            )
                .addOnSuccessListener { cont.resume(Unit) }
                .addOnFailureListener { cont.resumeWithException(it) }
        }

    suspend fun getCoupleById(coupleId: String): Couple? =
        suspendCancellableCoroutine { cont ->
            coupleRef(coupleId).get()
                .addOnSuccessListener { snap ->
                    if (!snap.exists()) { cont.resume(null); return@addOnSuccessListener }
                    cont.resume(snap.toCouple())
                }
                .addOnFailureListener { cont.resumeWithException(it) }
        }

    suspend fun updateStreak(coupleId: String) {
        val snap = suspendCancellableCoroutine<DocumentSnapshot> { cont ->
            coupleRef(coupleId).get()
                .addOnSuccessListener { cont.resume(it) }
                .addOnFailureListener { cont.resumeWithException(it) }
        }
        val lastAnsweredAt = snap.millisOrNull("lastAnsweredAt") ?: 0L
        val now = System.currentTimeMillis()
        val current = (snap.getLong("streakCount") ?: 0L).toInt()
        val newStreak = if (lastAnsweredAt == 0L || now - lastAnsweredAt > TWO_DAYS_MS) 1 else current + 1
        suspendCancellableCoroutine<Unit> { cont ->
            coupleRef(coupleId).set(
                mapOf("streakCount" to newStreak, "lastAnsweredAt" to now),
                SetOptions.merge()
            )
                .addOnSuccessListener { cont.resume(Unit) }
                .addOnFailureListener { cont.resumeWithException(it) }
        }
    }

    suspend fun leaveCouple() {
        // The client cannot batch-update the partner's user doc (rules deny cross-user writes).
        // Delegate to the leaveCoupleCallable Cloud Function which uses the Admin SDK.
        functions.getHttpsCallable("leaveCoupleCallable").call().await()
    }

    @Suppress("UNCHECKED_CAST")
    private fun DocumentSnapshot.toCouple() = Couple(
        id = id,
        userIds = (get("userIds") as? List<String>) ?: emptyList(),
        inviteCode = getString("inviteCode") ?: "",
        createdAt = millisOrNull("createdAt") ?: 0L,
        currentQuestionId = getString("currentQuestionId"),
        streakCount = (getLong("streakCount") ?: 0L).toInt(),
        lastAnsweredAt = millisOrNull("lastAnsweredAt"),
        activePackId = getString("activePackId"),
        encryptionVersion = (getLong("encryptionVersion") ?: 0L).toInt(),
        wrappedCoupleKey = getString("wrappedCoupleKey"),
        kdfSalt = getString("kdfSalt"),
        kdfParams = getString("kdfParams")
    )

    private fun DocumentSnapshot.millisOrNull(field: String): Long? = when (val raw = get(field)) {
        is Number -> raw.toLong()
        is Timestamp -> raw.toDate().time
        else -> null
    }

    companion object {
        private const val TWO_DAYS_MS = 48L * 60 * 60 * 1000
    }
}
