package net.buzzert.kordophone.backend.server

import kotlinx.coroutines.runBlocking
import net.buzzert.kordophone.backend.model.Conversation
import net.buzzert.kordophone.backend.model.GUID
import net.buzzert.kordophone.backend.model.Message
import okhttp3.Authenticator
import okhttp3.HttpUrl
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody
import okhttp3.Response
import okhttp3.ResponseBody
import okhttp3.Route
import okhttp3.WebSocket
import okhttp3.WebSocketListener
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.net.URL

interface APIClient {
    val isConfigured: Boolean

    fun getAPIInterface(): APIInterface
    fun getWebSocketClient(
        serverPath: String,
        queryParams: Map<String, String>?,
        listener: WebSocketListener
    ): WebSocket
}

data class Authentication (
    val username: String,
    val password: String,
)

class TokenStore(val authentication: Authentication) {
    var authenticationToken: String? = null
}

class AuthenticationInterceptor(
    val tokenStore: TokenStore
): Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        // If empty, allow the 401 to occur so we renew our token.
        val token = tokenStore.authenticationToken ?:
            return chain.proceed(chain.request())

        val newRequest = chain.request().newBuilder()
            .header("Authorization", "Bearer $token")
            .build()

        return chain.proceed(newRequest)
    }
}

class TokenAuthenticator(
    private val tokenStore: TokenStore,
    private val baseURL: URL
) : Authenticator {
    private val retrofit: Retrofit = Retrofit.Builder()
        .baseUrl(baseURL)
        .addConverterFactory(GsonConverterFactory.create())
        .build()

    private val apiInterface: APIInterface
        get() = retrofit.create(APIInterface::class.java)

    override fun authenticate(route: Route?, response: Response): Request? {
        // Fetch new token
        val request = AuthenticationRequest(
            username = tokenStore.authentication.username,
            password = tokenStore.authentication.password
        )

        val token = runBlocking {
            apiInterface.authenticate(request).body()
        }

        when (token) {
            null -> {
                // Auth failure.
                // TODO: How to bubble this up?
                return null
            }

            // Update token store
            else -> {
                tokenStore.authenticationToken = token.serializedToken

                return response.request().newBuilder()
                    .header("Authorization", "Bearer ${token.serializedToken}")
                    .build()
            }
        }
    }
}

class APIClientFactory {
    companion object {
        fun createClient(serverString: String?, authentication: Authentication?): APIClient {
            if (serverString == null || authentication == null) {
                return InvalidConfigurationAPIClient(InvalidConfigurationAPIClient.Issue.NOT_CONFIGURED)
            }

            val normalizedServerString = serverString.ensureTrailingSlash()

            // Try to parse server string
            val serverURL = HttpUrl.parse(normalizedServerString)
                ?: return InvalidConfigurationAPIClient(InvalidConfigurationAPIClient.Issue.INVALID_HOST_URL)

            return RetrofitAPIClient(serverURL.url(), authentication)
        }
    }
}

private fun String.ensureTrailingSlash(): String {
    return if (endsWith("/")) this else "$this/"
}

// TODO: Is this a dumb idea?
class InvalidConfigurationAPIClient(val issue: Issue): APIClient {
    enum class Issue {
        NOT_CONFIGURED,
        INVALID_CONFIGURATION,
        INVALID_HOST_URL,
    }

    class NotConfiguredError: Throwable(message = "Not configured.")
    class InvalidConfigurationError(submessage: String): Throwable(message = "Invalid configuration: $submessage")

    private class InvalidConfigurationAPIInterface(val issue: Issue): APIInterface {
        private fun throwError(): Nothing {
            when (issue) {
                Issue.NOT_CONFIGURED -> throw NotConfiguredError()
                Issue.INVALID_CONFIGURATION -> throw InvalidConfigurationError("Unknown.")
                Issue.INVALID_HOST_URL -> throw InvalidConfigurationError("Invalid host URL.")
            }
        }

        override suspend fun getVersion(): ResponseBody = throwError()
        override suspend fun getConversations(): retrofit2.Response<List<Conversation>> = throwError()
        override suspend fun sendMessage(request: SendMessageRequest): retrofit2.Response<SendMessageResponse> = throwError()
        override suspend fun markConversation(conversationGUID: String): retrofit2.Response<Void> = throwError()
        override suspend fun fetchAttachment(guid: String, preview: Boolean): ResponseBody = throwError()
        override suspend fun uploadAttachment(filename: String, body: RequestBody): retrofit2.Response<UploadAttachmentResponse> = throwError()
        override suspend fun authenticate(request: AuthenticationRequest): retrofit2.Response<AuthenticationResponse> = throwError()
        override suspend fun getMessages(conversationGUID: String, limit: Int?, beforeMessageGUID: GUID?, afterMessageGUID: GUID?): retrofit2.Response<List<Message>> = throwError()
    }

    override val isConfigured: Boolean
        get() { return issue != Issue.NOT_CONFIGURED }

    override fun getAPIInterface(): APIInterface {
        return InvalidConfigurationAPIInterface(issue)
    }

    override fun getWebSocketClient(
        serverPath: String,
        queryParams: Map<String, String>?,
        listener: WebSocketListener
    ): WebSocket {
        throw Error("Invalid configuration.")
    }
}

class RetrofitAPIClient(
    private val baseURL: URL,
    private val authentication: Authentication,
): APIClient {
    private val tokenStore: TokenStore = TokenStore(authentication)

    private val client: OkHttpClient = OkHttpClient.Builder()
        .addInterceptor(AuthenticationInterceptor(tokenStore))
        .authenticator(TokenAuthenticator(tokenStore, baseURL))
        .build()

    private val retrofit: Retrofit = Retrofit.Builder()
        .client(client)
        .baseUrl(baseURL)
        .addConverterFactory(GsonConverterFactory.create())
        .build()

    override val isConfigured: Boolean
        get() = true

    override fun getAPIInterface(): APIInterface {
        return retrofit.create(APIInterface::class.java)
    }

    override fun getWebSocketClient(
        serverPath: String,
        queryParams: Map<String, String>?,
        listener: WebSocketListener
    ): WebSocket {
        val params = (queryParams ?: hashMapOf<String, String>()).toMutableMap()

        val authToken = tokenStore.authenticationToken
        if (authToken != null) {
            params["token"] = authToken
        }

        val requestURL = baseURL.authenticatedWebSocketURL(serverPath, params)
        val request = Request.Builder()
            .url(requestURL)
            .build()

        return client.newWebSocket(request, listener)
    }
}

fun URL.authenticatedWebSocketURL(serverPath: String, params: Map<String, String>): URL {
    val baseURI = HttpUrl.parse(this.toString())!!
    val requestURL = baseURI.newBuilder()
        .host(baseURI.host())
        .addEncodedPathSegments(serverPath)

    params.forEach { (key, value) ->
        requestURL.addQueryParameter(key, value)
    }

    return URL(requestURL.build().toString())
}
