package app.closer.crypto

import app.closer.domain.model.Couple
import com.google.crypto.tink.Aead
import com.google.crypto.tink.KeysetHandle
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import javax.inject.Inject
import javax.inject.Singleton

enum class EncryptionStatus {
    /** Local keyset present -- ready to encrypt/decrypt. */
    UNLOCKED,
    /** Found keyset in the invite slot; moved to coupleId slot automatically. */
    RECONCILED_FROM_INVITE,
    /** encryptionVersion == 1 but no local keyset -- prompt for recovery phrase. */
    NEEDS_RECOVERY,
    /** encryptionVersion == 0 -- this couple must create a key before writing more answers. */
    NEEDS_ENCRYPTION_UPGRADE,
    /** encryptionVersion == 1 with a local key -- this device must rewrite its legacy answers. */
    NEEDS_CONTENT_MIGRATION
}

class MissingCoupleKeyException(coupleId: String) :
    IllegalStateException("Encrypted couple key is unavailable for $coupleId")

data class SetupResult(
    val handle: KeysetHandle,
    val wrapped: RecoveryKeyManager.WrappedKey,
    val recoveryPhrase: String
)

/**
 * High-level E2EE orchestration injected into repositories and ViewModels.
 * All KDF operations run on [Dispatchers.Default] to keep the main thread free.
 */
@Singleton
class CoupleEncryptionManager @Inject constructor(
    private val keyStore: CoupleKeyStore,
    private val keyManager: RecoveryKeyManager
) {
    /**
     * Called by the inviter when creating an invite.
     * Generates a new couple keyset + recovery phrase, wraps the keyset,
     * and stores it locally under the invite slot (coupleId unknown yet).
     */
    suspend fun setupForNewCouple(inviteCode: String): SetupResult = withContext(Dispatchers.Default) {
        val phrase = keyManager.generateRecoveryPhrase()
        val handle = keyManager.newCoupleKeyset()
        val wrapped = keyManager.wrap(handle, phrase)
        keyStore.storeInviteKeyset(inviteCode, handle)
        SetupResult(handle, wrapped, phrase)
    }

    /**
     * Called by the acceptor during pairing, and on any device during recovery.
     * Derives the wrap key from [phrase], unwraps the keyset, and stores it
     * locally under [coupleId].
     * Throws [com.google.crypto.tink.subtle.Validators]-wrapped exception if phrase is wrong.
     */
    suspend fun unwrapAndStore(
        coupleId: String,
        wrapped: RecoveryKeyManager.WrappedKey,
        phrase: String
    ): Result<Unit> = withContext(Dispatchers.Default) {
        runCatching {
            val handle = keyManager.unwrap(wrapped, phrase)
            keyStore.storeKeyset(coupleId, handle)
        }
    }

    /**
     * Called on app launch / Home load after the couple doc is resolved.
     * Handles inviter reconciliation (flow B') transparently.
     */
    fun checkStatus(couple: Couple): EncryptionStatus {
        if (couple.encryptionVersion == 0) return EncryptionStatus.NEEDS_ENCRYPTION_UPGRADE
        if (keyStore.hasKeyset(couple.id)) {
            return if (couple.encryptionVersion >= STRICT_ENCRYPTION_VERSION) {
                EncryptionStatus.UNLOCKED
            } else {
                EncryptionStatus.NEEDS_CONTENT_MIGRATION
            }
        }
        if (keyStore.reconcileInviteKeyset(couple.inviteCode, couple.id)) {
            return if (couple.encryptionVersion >= STRICT_ENCRYPTION_VERSION) {
                EncryptionStatus.RECONCILED_FROM_INVITE
            } else {
                EncryptionStatus.NEEDS_CONTENT_MIGRATION
            }
        }
        return EncryptionStatus.NEEDS_RECOVERY
    }

    fun aeadFor(coupleId: String): Aead? = keyStore.aeadFor(coupleId)

    /** Answer-bearing writes must never fall back to plaintext. */
    fun requireAead(coupleId: String): Aead =
        keyStore.aeadFor(coupleId) ?: throw MissingCoupleKeyException(coupleId)

    fun isUnlocked(coupleId: String): Boolean = keyStore.hasKeyset(coupleId)

    /**
     * Re-wraps the locally-held keyset with a new phrase and returns the new WrappedKey
     * so the caller can persist it to Firestore. The old phrase is NOT required.
     * Returns failure if no local keyset exists for [coupleId].
     */
    suspend fun rewrapWithNewPhrase(
        coupleId: String,
        newPhrase: String
    ): Result<RecoveryKeyManager.WrappedKey> = withContext(Dispatchers.Default) {
        runCatching {
            val handle = keyStore.loadKeyset(coupleId)
                ?: error("No local keyset for $coupleId -- cannot change phrase without recovery first")
            keyManager.wrap(handle, newPhrase)
        }
    }

    fun deleteKeyset(coupleId: String) = keyStore.deleteKeyset(coupleId)

    suspend fun setupLegacyCouple(coupleId: String): SetupResult = withContext(Dispatchers.Default) {
        val phrase = keyManager.generateRecoveryPhrase()
        val handle = keyManager.newCoupleKeyset()
        val wrapped = keyManager.wrap(handle, phrase)
        keyStore.storeKeyset(coupleId, handle)
        keyStore.storePendingRecoveryPhrase(coupleId, phrase)
        SetupResult(handle, wrapped, phrase)
    }

    fun pendingRecoveryPhrase(coupleId: String): String? =
        keyStore.pendingRecoveryPhrase(coupleId)

    fun acknowledgeRecoveryPhrase(coupleId: String) =
        keyStore.clearPendingRecoveryPhrase(coupleId)

    companion object {
        const val STRICT_ENCRYPTION_VERSION = 2
    }
}
