/*
 * SPDX-License-Identifier: GPL-3.0-or-later
 */
package com.runicgateway.app.core.net

import com.runicgateway.app.core.auth.SessionManager
import com.runicgateway.app.data.api.AuthRefreshApi
import com.runicgateway.app.data.api.dto.MobileRefreshRequest
import okhttp3.Authenticator
import okhttp3.Request
import okhttp3.Response
import okhttp3.Route
import java.io.IOException
import javax.inject.Inject
import javax.inject.Singleton

/**
 * Transparently refreshes an expired access token on a bearer `401` and replays
 * the request (PLAN.md §4.1, §4.3). Refresh tokens are single-use and rotated, so
 * this is serialized behind a mutex: concurrent 401s trigger exactly one refresh
 * and the losers reuse its result. A refresh that comes back `401` means the
 * session is truly dead → sign out; a network error leaves the session intact so
 * a later call can retry.
 *
 * The refresh call runs on [AuthRefreshApi] (its own bare client with no
 * authenticator), so it can never recurse back into here.
 */
@Singleton
class TokenAuthenticator @Inject constructor(
    private val sessionManager: SessionManager,
    private val refreshApi: AuthRefreshApi,
) : Authenticator {

    private val lock = Any()

    override fun authenticate(route: Route?, response: Response): Request? {
        val failed = response.request
        // Credential endpoints (login/refresh) must never be "refreshed".
        if (failed.header(Http.NO_SESSION_HEADER) != null) return null
        // Give up after a single refresh+replay to avoid an auth loop.
        if (priorResponseCount(response) >= 2) return null

        val attemptedAuth = failed.header(Http.AUTHORIZATION)

        synchronized(lock) {
            // Another thread may have already refreshed while we waited on the lock.
            val current = sessionManager.currentAccessToken()
            if (current != null && Http.bearer(current) != attemptedAuth) {
                return failed.retryWith(current)
            }

            val refreshToken = sessionManager.currentRefreshToken()
                ?: return null // already signed out

            val refreshed = try {
                refreshApi.refresh(MobileRefreshRequest(refreshToken)).execute()
            } catch (_: IOException) {
                // Transient — surface the original 401 but keep the session.
                return null
            }

            val body = refreshed.body()
            if (!refreshed.isSuccessful || body == null) {
                // The refresh token is dead (401/expired/revoked) → session is over.
                sessionManager.onSignedOut()
                return null
            }

            sessionManager.onRefreshed(body.accessToken, body.refreshToken, body.user)
            return failed.retryWith(body.accessToken)
        }
    }

    private fun Request.retryWith(accessToken: String): Request =
        newBuilder().header(Http.AUTHORIZATION, Http.bearer(accessToken)).build()

    private fun priorResponseCount(response: Response): Int {
        var count = 1
        var prior = response.priorResponse
        while (prior != null) {
            count++
            prior = prior.priorResponse
        }
        return count
    }
}
