package dev.castarr.tv.auth

import android.content.Context
import android.util.Log
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.json.JSONObject
import java.net.HttpURLConnection
import java.net.URL
import java.net.URLEncoder

/**
 * OIDC device-authorization-grant client. The only configuration a user
 * enters is the Dispatcharr URL — issuer and public client id come from its
 * /api/accounts/oidc/status/ endpoint (fork feature).
 */
class DeviceAuth(context: Context) {

    private val prefs = context.getSharedPreferences("auth", Context.MODE_PRIVATE)

    data class ServerConfig(val issuer: String, val clientId: String, val label: String)
    data class DeviceSession(
        val deviceCode: String,
        val userCode: String,
        val verificationUri: String,
        val verificationUriComplete: String,
        val intervalSeconds: Int,
        val expiresInSeconds: Int,
    )

    sealed class PollResult {
        data object Pending : PollResult()
        data object Denied : PollResult()
        data class Success(val username: String) : PollResult()
    }

    var serverUrl: String
        get() = prefs.getString("server_url", "").orEmpty().trimEnd('/')
        set(value) { prefs.edit().putString("server_url", value.trimEnd('/')).apply() }

    val username: String get() = prefs.getString("username", "").orEmpty()
    val isLoggedIn: Boolean get() = prefs.getString("refresh_token", null) != null

    private var issuer: String
        get() = prefs.getString("issuer", "").orEmpty()
        set(value) { prefs.edit().putString("issuer", value).apply() }
    private var clientId: String
        get() = prefs.getString("client_id", "").orEmpty()
        set(value) { prefs.edit().putString("client_id", value).apply() }

    suspend fun fetchServerConfig(url: String): ServerConfig = withContext(Dispatchers.IO) {
        val status = JSONObject(get("${url.trimEnd('/')}/api/accounts/oidc/status/"))
        val config = ServerConfig(
            issuer = status.optString("issuer"),
            clientId = status.optString("device_client_id"),
            label = status.optString("label"),
        )
        require(status.optBoolean("enabled") && config.issuer.isNotEmpty() && config.clientId.isNotEmpty()) {
            "Server has no device-flow SSO configured"
        }
        serverUrl = url
        issuer = config.issuer
        clientId = config.clientId
        config
    }

    private suspend fun discovery(): JSONObject = withContext(Dispatchers.IO) {
        JSONObject(get(issuer.trimEnd('/') + "/.well-known/openid-configuration"))
    }

    suspend fun startDeviceFlow(): DeviceSession = withContext(Dispatchers.IO) {
        val doc = discovery()
        val response = JSONObject(
            post(
                doc.getString("device_authorization_endpoint"),
                mapOf("client_id" to clientId, "scope" to "openid profile email offline_access"),
            )
        )
        DeviceSession(
            deviceCode = response.getString("device_code"),
            userCode = response.getString("user_code"),
            verificationUri = response.optString("verification_uri"),
            verificationUriComplete = response.optString(
                "verification_uri_complete",
                response.optString("verification_uri"),
            ),
            intervalSeconds = response.optInt("interval", 5).coerceAtLeast(2),
            expiresInSeconds = response.optInt("expires_in", 600),
        )
    }

    suspend fun poll(session: DeviceSession): PollResult = withContext(Dispatchers.IO) {
        val doc = discovery()
        val body = post(
            doc.getString("token_endpoint"),
            mapOf(
                "grant_type" to "urn:ietf:params:oauth:grant-type:device_code",
                "device_code" to session.deviceCode,
                "client_id" to clientId,
            ),
            allowError = true,
        )
        val json = JSONObject(body)
        when {
            json.has("access_token") -> {
                storeTokens(json)
                PollResult.Success(username)
            }
            json.optString("error") in listOf("authorization_pending", "slow_down") -> PollResult.Pending
            else -> {
                Log.w(TAG, "device flow error: ${json.optString("error")}")
                PollResult.Denied
            }
        }
    }

    /** Valid access token, refreshing when necessary; null when logged out. */
    suspend fun accessToken(): String? = withContext(Dispatchers.IO) {
        val token = prefs.getString("access_token", null)
        val expiry = prefs.getLong("expires_at", 0)
        if (token != null && System.currentTimeMillis() < expiry - 30_000) return@withContext token
        val refresh = prefs.getString("refresh_token", null) ?: return@withContext null
        runCatching {
            val doc = discovery()
            val json = JSONObject(
                post(
                    doc.getString("token_endpoint"),
                    mapOf(
                        "grant_type" to "refresh_token",
                        "refresh_token" to refresh,
                        "client_id" to clientId,
                    ),
                )
            )
            storeTokens(json)
            json.getString("access_token")
        }.onFailure { Log.w(TAG, "token refresh failed", it) }.getOrNull()
    }

    fun logout() {
        prefs.edit()
            .remove("access_token").remove("refresh_token")
            .remove("expires_at").remove("username")
            .apply()
    }

    private fun storeTokens(json: JSONObject) {
        val editor = prefs.edit()
            .putString("access_token", json.getString("access_token"))
            .putLong(
                "expires_at",
                System.currentTimeMillis() + json.optLong("expires_in", 3600) * 1000,
            )
        if (json.has("refresh_token")) editor.putString("refresh_token", json.getString("refresh_token"))
        // Username from the id_token payload (unverified decode is fine here —
        // it arrived over TLS directly from the token endpoint).
        json.optString("id_token").split(".").getOrNull(1)?.let { payload ->
            runCatching {
                val decoded = String(android.util.Base64.decode(payload, android.util.Base64.URL_SAFE))
                val claims = JSONObject(decoded)
                editor.putString(
                    "username",
                    claims.optString("preferred_username", claims.optString("sub")),
                )
            }
        }
        editor.apply()
    }

    private fun get(url: String): String {
        val connection = URL(url).openConnection() as HttpURLConnection
        return try {
            connection.connectTimeout = 10_000
            connection.readTimeout = 15_000
            connection.inputStream.bufferedReader().use { it.readText() }
        } finally {
            connection.disconnect()
        }
    }

    private fun post(url: String, form: Map<String, String>, allowError: Boolean = false): String {
        val connection = URL(url).openConnection() as HttpURLConnection
        return try {
            connection.requestMethod = "POST"
            connection.doOutput = true
            connection.connectTimeout = 10_000
            connection.readTimeout = 15_000
            connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded")
            val body = form.entries.joinToString("&") {
                "${it.key}=${URLEncoder.encode(it.value, "UTF-8")}"
            }
            connection.outputStream.use { it.write(body.toByteArray()) }
            val stream = if (connection.responseCode >= 400) {
                if (!allowError) error("HTTP ${connection.responseCode} from $url")
                connection.errorStream
            } else {
                connection.inputStream
            }
            stream.bufferedReader().use { it.readText() }
        } finally {
            connection.disconnect()
        }
    }

    private companion object {
        const val TAG = "DeviceAuth"
    }
}
