package dev.privacyllc.period.core.security

/**
 * The two clocks this feature needs, behind an interface so the awkward cases
 * are testable without a device and without waiting fifteen minutes.
 *
 * Neither clock is sufficient alone, and that is the whole reason this type
 * exists:
 *
 * - **Wall time** is user-settable. Someone locked out for fifteen minutes can
 *   open Settings, move the date forward, and come back to an expired lockout.
 * - **Elapsed real time** is monotonic and cannot be set — but it resets to zero
 *   on reboot, so it forgets every deadline the moment the phone restarts.
 *
 * Used together they cover each other, provided the answer is always the
 * *longer* remaining wait rather than the shorter one.
 */
internal interface Clocks {
    /** `System.currentTimeMillis()` — settable by the user. */
    fun wallMillis(): Long

    /** `SystemClock.elapsedRealtime()` — monotonic, resets on boot. */
    fun elapsedMillis(): Long
}

/**
 * How many wrong PINs there have been, and until when.
 *
 * Persisted with a MAC over it (tag `0x02`), so an edit is detectable. It is
 * not tamper-*proof* and does not need to be: anyone who can rewrite this app's
 * private files already has root and can read the unencrypted cycle database
 * directly, and Android's own Settings → Clear storage resets this counter while
 * destroying the database in the same action. The MAC is there so tampering
 * fails **closed** rather than silently succeeding.
 */
internal data class LockoutState(
    val failedAttempts: Int = 0,
    /** Wall-clock instant the lockout ends. Meaningless if the user moves the clock. */
    val wallDeadline: Long = 0L,
    /** Elapsed-time instant the lockout ends. Meaningless after a reboot. */
    val elapsedDeadline: Long = 0L,
    /** Elapsed reading when the deadline was written. A smaller reading now means a reboot. */
    val elapsedMark: Long = 0L,
) {
    fun encode(): String = "$VERSION|$failedAttempts|$wallDeadline|$elapsedDeadline|$elapsedMark"

    companion object {
        const val VERSION = "v1"

        /** Null for anything unparseable. The caller treats null as maximum backoff, never as zero. */
        fun decode(raw: String?): LockoutState? {
            val p = raw?.split("|") ?: return null
            if (p.size != 5 || p[0] != VERSION) return null
            return LockoutState(
                failedAttempts = p[1].toIntOrNull()?.takeIf { it >= 0 } ?: return null,
                wallDeadline = p[2].toLongOrNull() ?: return null,
                elapsedDeadline = p[3].toLongOrNull() ?: return null,
                elapsedMark = p[4].toLongOrNull() ?: return null,
            )
        }
    }
}
