package com.ponzischeme89.memby.data.remote

import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
import com.ponzischeme89.memby.BuildConfig
import com.ponzischeme89.memby.ServiceLocator
import com.ponzischeme89.memby.data.audioPassthroughPreference
import com.ponzischeme89.memby.data.playback.deviceAudioCapabilities
import com.ponzischeme89.memby.data.playback.devicePlaybackCapabilities
import com.ponzischeme89.memby.data.playback.gatewayAudioTokens
import com.ponzischeme89.memby.data.playback.gatewayCapabilityTokens
import com.ponzischeme89.memby.update.RequiredUpdateSignal
import kotlinx.serialization.json.Json
import okhttp3.Interceptor
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.Response
import retrofit2.Retrofit
import java.util.concurrent.TimeUnit

/** Builds a [GatewayApi] bound to a Memby gateway. */
object GatewayServiceFactory {

    private val json = Json {
        ignoreUnknownKeys = true
        coerceInputValues = true
        isLenient = true
        explicitNulls = false
    }

    fun create(baseUrl: String, tokenProvider: () -> String?): GatewayApi {
        val contentType = "application/json".toMediaType()

        // Derived from the shared stack: artwork in gateway mode is proxied by this very
        // host, so the poster fetches reuse the connection this client established.
        val client = HttpStack.base.newBuilder()
            .connectTimeout(10, TimeUnit.SECONDS)
            // The gateway answers home from Redis in single-digit milliseconds; a long
            // read timeout here only ever means Emby itself is struggling behind it.
            .readTimeout(20, TimeUnit.SECONDS)
            .addInterceptor(GatewayAuthInterceptor(tokenProvider))
            .addInterceptor(RequiredUpdateInterceptor)
            .build()

        return Retrofit.Builder()
            .baseUrl(baseUrl.ensureTrailingSlash())
            .client(client)
            .addConverterFactory(json.asConverterFactory(contentType))
            .build()
            .create(GatewayApi::class.java)
    }
}

/**
 * Sends the gateway token as a bearer header. Image URLs cannot carry headers, so those
 * are built with a `t=` query parameter instead (see EmbyRepository's URL helpers).
 */
private class GatewayAuthInterceptor(private val tokenProvider: () -> String?) : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val builder = chain.request().newBuilder()
            .header("Accept", "application/json")
            // The gateway decides whether this build needs updating, so every request
            // says which build it is.
            .header("X-Memby-Version", BuildConfig.VERSION_NAME)
            .header("X-Memby-Protocol", MEMBY_PROTOCOL_VERSION.toString())
            .header(
                "X-Memby-Capabilities",
                (
                    MEMBY_CAPABILITIES +
                        devicePlaybackCapabilities.gatewayCapabilityTokens() +
                        audioCapabilityTokens()
                    ).joinToString(","),
            )
        tokenProvider()?.takeIf { it.isNotBlank() }?.let {
            builder.header("Authorization", "Bearer $it")
        }
        return chain.proceed(builder.build())
    }

    /**
     * What this set can put through its speakers, resolved against the viewer's own
     * passthrough choice — so a manual override reaches the *server's* device profile too,
     * not only the audio sink. Read on each request rather than captured, because the
     * override can be changed in Settings without restarting the process.
     *
     * It is guarded because this interceptor also runs before the service locator exists
     * in a screenshot or instrumentation context, where an exception here would fail the
     * request rather than merely describe the television modestly.
     */
    private fun audioCapabilityTokens(): List<String> = runCatching {
        deviceAudioCapabilities.gatewayAudioTokens(
            ServiceLocator.settings.current.audioPassthroughPreference,
        )
    }.getOrDefault(emptyList())
}

/**
 * Watches every gateway response for the header that says this build has been retired.
 *
 * It is an interceptor rather than a check at the call sites because the refusal arrives
 * on whichever request happened to be in flight — the status poll, a home refresh, a
 * sign-in — and only one of those has any reason to know about update policy. Reading the
 * header here is also what makes the 401 legible: on its own it is indistinguishable from
 * an expired session, which is why the app used to answer it with a sign-in form the
 * gateway would then refuse.
 */
private object RequiredUpdateInterceptor : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val response = chain.proceed(chain.request())
        response.header(RequiredUpdateSignal.HEADER)?.let(RequiredUpdateSignal::report)
        return response
    }
}

internal const val MEMBY_PROTOCOL_VERSION = 1

internal val MEMBY_CAPABILITIES = listOf(
    "server_features_v1",
    "live_feature_refresh_v1",
    "sonarr_preroll_v1",
    "auto_my_shows_v1",
    "genre_browser_v1",
    // Declares that this build has the TV calendar destination, so the operator cannot
    // push a rail entry to a television that has nowhere to send it.
    "tv_calendar_v1",
    // Declares that this build can show the install-permission step. An older app never
    // receives the feature, so the operator cannot push a screen it does not have.
    "install_permission_v1",
    // Declares that this build can draw seek preview thumbnails, so the admin console
    // reports the feature honestly against an older app that would never ask for them.
    "trickplay_v1",
    // Declares that this build can offer to skip an episode's opening titles, so the admin
    // console reports the feature honestly against an older app that would never ask.
    "skip_intro_v1",
    // Declares that this build reports what its receiver accepts and can be sent a stream
    // whose audio it will bitstream. A gateway seeing this knows the audio tokens beside
    // it are a complete answer rather than an older app that simply never described itself.
    "audio_passthrough_v1",
)

internal const val HEVC_DECODE_CAPABILITY = "video_hevc_decode"
