package app.closer.data.repository

import app.closer.crypto.CoupleEncryptionManager
import app.closer.crypto.RecoveryKeyManager
import app.closer.data.local.PendingInviteStore
import app.closer.data.local.RecoveryPhraseStore
import app.closer.data.remote.FirestoreInviteDataSource
import app.closer.domain.repository.AcceptInviteResult
import app.closer.domain.repository.CreateInviteResult
import app.closer.domain.repository.InviteRepository
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.random.Random

@Singleton
class InviteRepositoryImpl @Inject constructor(
    private val dataSource: FirestoreInviteDataSource,
    private val encryptionManager: CoupleEncryptionManager,
    private val keyManager: RecoveryKeyManager,
    private val recoveryPhraseStore: RecoveryPhraseStore,
    private val pendingInviteStore: PendingInviteStore
) : InviteRepository {

    override suspend fun createInvite(): Result<CreateInviteResult> = runCatching {
        // Return the cached invite if it's still within the 24-hour window, avoiding redundant
        // Cloud Function calls and the server-side rate limit (5 per hour).
        pendingInviteStore.load()?.let { cached ->
            return@runCatching CreateInviteResult(cached.code, cached.recoveryPhrase)
        }

        // Generate the keyset + phrase once. Only the code (and the per-code encrypted phrase)
        // changes on the rare collision retry — the keyset itself stays the same.
        val setup = encryptionManager.setupForNewCouple()
        var lastError: Throwable? = null
        for (attempt in 0 until MAX_CODE_ATTEMPTS) {
            val code = generateCode()
            val encryptedPhrase = keyManager.encryptPhraseWithCode(code, setup.recoveryPhrase)
            val response = runCatching { dataSource.createInvite(code, setup.wrapped, encryptedPhrase) }
            response.onSuccess { r ->
                encryptionManager.storeInviteSetup(r.code, setup)
                pendingInviteStore.save(r.code, setup.recoveryPhrase)
                recoveryPhraseStore.save(setup.recoveryPhrase)
                return@runCatching CreateInviteResult(r.code, setup.recoveryPhrase)
            }
            response.onFailure { e ->
                if (!isCodeConflict(e)) throw e
                lastError = e
            }
        }
        throw lastError ?: IllegalStateException("Could not generate a unique invite code")
    }

    override suspend fun acceptInvite(code: String): Result<AcceptInviteResult> = runCatching {
        val raw = dataSource.acceptInvite(code)
        // raw.recoveryPhrase holds the encrypted blob from the server; decrypt it here so
        // callers always receive the plaintext phrase (or null for iOS/plaintext couples).
        val phrase = raw.recoveryPhrase?.let {
            runCatching { keyManager.decryptPhraseWithCode(code, it) }.getOrNull()
        }
        phrase?.let { recoveryPhraseStore.save(it) }
        pendingInviteStore.clear()
        raw.copy(recoveryPhrase = phrase)
    }

    override fun watchAccepted(code: String): Flow<String?> = dataSource.watchInviteAccepted(code)

    private fun isCodeConflict(e: Throwable): Boolean {
        val msg = e.message ?: return false
        return msg.contains("already-exists", ignoreCase = true) ||
               msg.contains("ALREADY_EXISTS", ignoreCase = true)
    }

    private fun generateCode(): String = (1..CODE_LENGTH)
        .map { CODE_CHARS[Random.nextInt(CODE_CHARS.length)] }
        .joinToString("")

    companion object {
        private const val CODE_CHARS = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"
        private const val CODE_LENGTH = 6
        private const val MAX_CODE_ATTEMPTS = 10
    }
}
