package dev.castarr.tv.pairing

import android.content.Context
import java.net.Inet4Address
import java.net.NetworkInterface
import java.security.MessageDigest
import java.security.SecureRandom

object Pairing {

    const val PORT = 8765

    private const val PREFS = "pairing"
    private const val KEY_CODE = "code"
    private const val KEY_TOKEN = "token"
    private const val KEY_SESSIONS = "session_tokens"
    private const val MAX_SESSIONS = 8

    private fun prefs(context: Context) =
        context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)

    /**
     * Four-digit pairing code, generated once and kept stable so an already
     * paired phone survives app restarts. Human fallback only — the QR code
     * carries the long token below.
     */
    fun code(context: Context): String {
        prefs(context).getString(KEY_CODE, null)?.let { return it }
        val code = "%04d".format(SecureRandom().nextInt(10_000))
        prefs(context).edit().putString(KEY_CODE, code).apply()
        return code
    }

    /** 128-bit random token embedded in the QR code; not brute-forceable. */
    fun token(context: Context): String {
        prefs(context).getString(KEY_TOKEN, null)?.let { return it }
        val token = randomToken()
        prefs(context).edit().putString(KEY_TOKEN, token).apply()
        return token
    }

    /**
     * Issues a separate token for a client that authenticated with the
     * four-digit code. Handing out the QR token there would make the
     * brute-forceable path yield the un-rotating master credential.
     */
    fun newSessionToken(context: Context): String {
        val token = randomToken()
        val sessions = sessionTokens(context).toMutableList()
        sessions.add(token)
        while (sessions.size > MAX_SESSIONS) sessions.removeAt(0)
        prefs(context).edit().putStringSet(KEY_SESSIONS, sessions.toSet()).apply()
        return token
    }

    /** Constant-time check against the QR token and all session tokens. */
    fun isValidToken(context: Context, candidate: String): Boolean {
        if (candidate.isEmpty()) return false
        if (constantTimeEquals(candidate, token(context))) return true
        return sessionTokens(context).any { constantTimeEquals(candidate, it) }
    }

    fun isValidCode(context: Context, candidate: String): Boolean =
        candidate.isNotEmpty() && constantTimeEquals(candidate, code(context))

    /**
     * Drops every credential: the QR token, the code and all session tokens.
     * Paired phones must scan again — the way to revoke a leaked token.
     */
    fun reset(context: Context) {
        prefs(context).edit().clear().apply()
    }

    private fun sessionTokens(context: Context): List<String> =
        prefs(context).getStringSet(KEY_SESSIONS, emptySet())?.toList().orEmpty()

    private fun randomToken(): String =
        ByteArray(16).also { SecureRandom().nextBytes(it) }
            .joinToString("") { "%02x".format(it) }

    private fun constantTimeEquals(a: String, b: String): Boolean =
        MessageDigest.isEqual(a.toByteArray(), b.toByteArray())

    /** Best-guess LAN IPv4 address of this device, or null when offline. */
    fun lanAddress(): String? {
        val candidates = runCatching {
            NetworkInterface.getNetworkInterfaces().asSequence()
                .filter { it.isUp && !it.isLoopback }
                .flatMap { it.inetAddresses.asSequence() }
                .filterIsInstance<Inet4Address>()
                .filter { it.isSiteLocalAddress }
                .map { it.hostAddress }
                .filterNotNull()
                .toList()
        }.getOrDefault(emptyList())
        return candidates.firstOrNull()
    }

    fun remoteUrl(address: String): String = "http://$address:$PORT"

    fun pairingUrl(context: Context, address: String): String =
        "${remoteUrl(address)}/?t=${token(context)}"
}
