package app.closer.domain.security

import kotlin.math.min
import kotlin.math.pow

/**
 * Per-session client-side rate limiter for Firebase Auth operations.
 *
 * Tracks failed attempts per flow (login, signup, password reset) and enforces
 * exponential backoff plus a hard lockout after [MAX_FAILURES_BEFORE_LOCKOUT]
 * failures. Counters reset automatically on the next successful auth attempt.
 *
 * This lives in memory only; it is not a substitute for server-side rate
 * limiting, but it stops naive credential stuffing and accidental UI spam.
 */
object AuthRateLimiter {

    /** Number of failures before the first backoff delay is enforced. */
    const val SOFT_LIMIT_FAILURES = 3

    /** Number of failures after which the account/flow is locked out for [LOCKOUT_MS]. */
    const val MAX_FAILURES_BEFORE_LOCKOUT = 5

    /** Hard lockout duration in milliseconds. */
    const val LOCKOUT_MS = 30_000L

    /** Maximum backoff delay between attempts in milliseconds. */
    private const val MAX_BACKOFF_MS = 8_000L

    /** Initial backoff delay after crossing [SOFT_LIMIT_FAILURES]. */
    private const val BASE_BACKOFF_MS = 1_000L

    /** Backoff exponent: delay = [BASE_BACKOFF_MS] * 2^(failureCount - [SOFT_LIMIT_FAILURES]). */
    private const val BACKOFF_EXPONENT_BASE = 2.0

    enum class Flow {
        LOGIN,
        SIGN_UP,
        PASSWORD_RESET,
        ANONYMOUS
    }

    private data class AttemptState(
        val failures: Int = 0,
        val lockoutEndMs: Long = 0L
    )

    private val states = mutableMapOf<Flow, AttemptState>()

    /**
     * Returns the number of milliseconds the caller must wait before the next
     * attempt for [flow], or 0 if the attempt may proceed immediately.
     */
    fun timeUntilNextAttemptMs(flow: Flow = Flow.LOGIN): Long {
        val now = System.currentTimeMillis()
        val state = synchronized(states) { states[flow] ?: AttemptState() }

        val remainingLockout = (state.lockoutEndMs - now).coerceAtLeast(0L)
        if (remainingLockout > 0L) {
            return remainingLockout
        }

        val requiredBackoff = computeBackoffDelayMs(state.failures)
        return requiredBackoff
    }

    /**
     * Records a failed attempt for [flow]. If this crosses the lockout threshold,
     * a hard lockout is started and the returned duration reflects the lockout.
     */
    fun recordFailure(flow: Flow = Flow.LOGIN): Long {
        val now = System.currentTimeMillis()
        synchronized(states) {
            val current = states[flow] ?: AttemptState()
            val newFailures = current.failures + 1
            val lockoutEndMs = if (newFailures >= MAX_FAILURES_BEFORE_LOCKOUT) {
                now + LOCKOUT_MS
            } else {
                0L
            }
            states[flow] = current.copy(
                failures = newFailures,
                lockoutEndMs = lockoutEndMs
            )
        }
        return timeUntilNextAttemptMs(flow)
    }

    /**
     * Resets the failure counter for [flow]. Call after a successful auth attempt.
     */
    fun recordSuccess(flow: Flow = Flow.LOGIN) {
        synchronized(states) {
            states.remove(flow)
        }
    }

    /**
     * Convenience helper that returns true when the caller must wait.
     */
    fun isThrottled(flow: Flow = Flow.LOGIN): Boolean =
        timeUntilNextAttemptMs(flow) > 0L

    /**
     * Returns a human-readable message for the current throttle state.
     */
    fun throttleMessage(flow: Flow = Flow.LOGIN): String? {
        val waitMs = timeUntilNextAttemptMs(flow)
        if (waitMs <= 0L) return null
        val seconds = (waitMs / 1_000L).coerceAtLeast(1L)
        return "Too many attempts. Try again in $seconds second${if (seconds == 1L) "" else "s"}."
    }

    private fun computeBackoffDelayMs(failures: Int): Long {
        if (failures < SOFT_LIMIT_FAILURES) return 0L
        val exponent = failures - SOFT_LIMIT_FAILURES
        val raw = BASE_BACKOFF_MS * BACKOFF_EXPONENT_BASE.pow(exponent)
        return min(raw.toLong(), MAX_BACKOFF_MS)
    }
}
