package bou.amine.apps.readerforselfossv2.rest

import bou.amine.apps.readerforselfossv2.model.SelfossModel
import bou.amine.apps.readerforselfossv2.model.StatusAndData
import bou.amine.apps.readerforselfossv2.model.SuccessResponse
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
import io.github.aakira.napier.Napier
import io.ktor.client.HttpClient
import io.ktor.client.engine.cio.CIO
import io.ktor.client.engine.cio.CIOEngineConfig
import io.ktor.client.plugins.HttpRequestRetry
import io.ktor.client.plugins.HttpTimeout
import io.ktor.client.plugins.auth.providers.BasicAuthCredentials
import io.ktor.client.plugins.cache.HttpCache
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.plugins.cookies.HttpCookies
import io.ktor.client.plugins.logging.LogLevel
import io.ktor.client.plugins.logging.Logger
import io.ktor.client.plugins.logging.Logging
import io.ktor.client.request.get
import io.ktor.client.request.headers
import io.ktor.client.request.parameter
import io.ktor.client.statement.HttpResponse
import io.ktor.http.HttpHeaders
import io.ktor.http.HttpStatusCode
import io.ktor.http.Parameters
import io.ktor.serialization.kotlinx.json.json
import io.ktor.util.encodeBase64
import io.ktor.utils.io.charsets.Charsets
import io.ktor.utils.io.core.toByteArray
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json

expect fun setupInsecureHTTPEngine(config: CIOEngineConfig)

class SelfossApi(private val appSettingsService: AppSettingsService) {
    var client = createHttpClient()

    fun createHttpClient() =
        HttpClient(CIO) {
            if (appSettingsService.getSelfSigned()) {
                engine {
                    setupInsecureHTTPEngine(this)
                }
            }
            install(HttpCache)
            install(ContentNegotiation) {
                json(
                    Json {
                        prettyPrint = true
                        isLenient = true
                        ignoreUnknownKeys = true
                        explicitNulls = false
                    },
                )
            }
            install(Logging) {
                logger =
                    object : Logger {
                        override fun log(message: String) {
                            Napier.d(message, tag = "LogApiCalls")
                        }
                    }
                level = LogLevel.INFO
            }
            install(HttpTimeout) {
                requestTimeoutMillis = appSettingsService.getApiTimeout()
            }
            install(HttpCookies)
            install(HttpRequestRetry) {
                maxRetries = 2
                retryIf { _, response ->
                    response.status == HttpStatusCode.Forbidden && shouldHavePostLogin() && hasLoginInfo()
                }
                modifyRequest {
                    Napier.i("Will modify", tag = "HttpSend")
                    CoroutineScope(Dispatchers.Main).launch {
                        Napier.i("Will login", tag = "HttpSend")
                        login()
                        Napier.i("Did login", tag = "HttpSend")
                    }
                }
            }
            expectSuccess = false
        }

    fun url(path: String) = "${appSettingsService.getBaseUrl()}$path"

    fun refreshLoginInformation() {
        appSettingsService.refreshApiSettings()
        client = createHttpClient()
    }

    fun constructBasicAuthValue(credentials: BasicAuthCredentials): String {
        val authString = "${credentials.username}:${credentials.password}"
        val authBuf = authString.toByteArray(Charsets.UTF_8).encodeBase64()

        return "Basic $authBuf"
    }

    // Api version was introduces after the POST login, so when there is a version, it should be available
    private fun shouldHavePostLogin() = appSettingsService.getApiVersion() != -1

    private fun hasLoginInfo() =
        appSettingsService.getUserName().isNotEmpty() &&
                appSettingsService.getPassword()
                    .isNotEmpty()

    suspend fun login(): SuccessResponse =
        if (appSettingsService.getUserName().isNotEmpty() &&
            appSettingsService.getPassword()
                .isNotEmpty()
        ) {
            if (shouldHavePostLogin()) {
                postLogin()
            } else {
                getLogin()
            }
        } else {
            SuccessResponse(true)
        }

    private suspend fun getLogin() =
        maybeResponse(
            client.tryToGet(url("/login")) {
                parameter("username", appSettingsService.getUserName())
                parameter("password", appSettingsService.getPassword())
                if (appSettingsService.getBasicUserName()
                        .isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()
                ) {
                    headers {
                        append(
                            HttpHeaders.Authorization,
                            constructBasicAuthValue(
                                BasicAuthCredentials(
                                    username = appSettingsService.getBasicUserName(),
                                    password = appSettingsService.getBasicPassword(),
                                ),
                            ),
                        )
                    }
                }
            },
        )

    private suspend fun postLogin() =
        maybeResponse(
            client.tryToPost(url("/login")) {
                parameter("username", appSettingsService.getUserName())
                parameter("password", appSettingsService.getPassword())
                if (appSettingsService.getBasicUserName()
                        .isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()
                ) {
                    headers {
                        append(
                            HttpHeaders.Authorization,
                            constructBasicAuthValue(
                                BasicAuthCredentials(
                                    username = appSettingsService.getBasicUserName(),
                                    password = appSettingsService.getBasicPassword(),
                                ),
                            ),
                        )
                    }
                }
            },
        )

    private fun shouldHaveNewLogout() =
        appSettingsService.getApiVersion() >= 5 // We are missing 4.1.0

    suspend fun logout(): SuccessResponse =
        if (shouldHaveNewLogout()) {
            doLogout()
        } else {
            maybeLogoutIfAvailable()
        }

    private suspend fun maybeLogoutIfAvailable() =
        responseOrSuccessIf404(
            client.tryToGet(url("/logout")) {
                if (appSettingsService.getBasicUserName()
                        .isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()
                ) {
                    headers {
                        append(
                            HttpHeaders.Authorization,
                            constructBasicAuthValue(
                                BasicAuthCredentials(
                                    username = appSettingsService.getBasicUserName(),
                                    password = appSettingsService.getBasicPassword(),
                                ),
                            ),
                        )
                    }
                }
            },
        )

    private suspend fun doLogout() =
        maybeResponse(
            client.tryToDelete(url("/api/session/current")) {
                if (appSettingsService.getBasicUserName()
                        .isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()
                ) {
                    headers {
                        append(
                            HttpHeaders.Authorization,
                            constructBasicAuthValue(
                                BasicAuthCredentials(
                                    username = appSettingsService.getBasicUserName(),
                                    password = appSettingsService.getBasicPassword(),
                                ),
                            ),
                        )
                    }
                }
            },
        )

    suspend fun getItems(
        type: String,
        offset: Int,
        tag: String?,
        source: Long?,
        search: String?,
        updatedSince: String?,
        items: Int? = null,
    ): StatusAndData<List<SelfossModel.Item>> =
        bodyOrFailure(
            client.tryToGet(url("/items")) {
                if (!shouldHavePostLogin()) {
                    parameter("username", appSettingsService.getUserName())
                    parameter("password", appSettingsService.getPassword())
                }
                parameter("type", type)
                parameter("tag", tag)
                parameter("source", source)
                parameter("search", search)
                parameter("updatedsince", updatedSince)
                parameter("items", items ?: appSettingsService.getItemsNumber())
                parameter("offset", offset)
                if (appSettingsService.getBasicUserName()
                        .isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()
                ) {
                    headers {
                        append(
                            HttpHeaders.Authorization,
                            constructBasicAuthValue(
                                BasicAuthCredentials(
                                    username = appSettingsService.getBasicUserName(),
                                    password = appSettingsService.getBasicPassword(),
                                ),
                            ),
                        )
                    }
                }
            },
        )

    suspend fun getItemsWithoutCatch(): StatusAndData<List<SelfossModel.Item>> =
        bodyOrFailure(
            client.get(url("/items")) {
                if (!shouldHavePostLogin()) {
                    parameter("username", appSettingsService.getUserName())
                    parameter("password", appSettingsService.getPassword())
                }
                parameter("type", "all")
                parameter("items", 1)
                if (appSettingsService.getBasicUserName()
                        .isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()
                ) {
                    headers {
                        append(
                            HttpHeaders.Authorization,
                            constructBasicAuthValue(
                                BasicAuthCredentials(
                                    username = appSettingsService.getBasicUserName(),
                                    password = appSettingsService.getBasicPassword(),
                                ),
                            ),
                        )
                    }
                }
            },
        )

    suspend fun stats(): StatusAndData<SelfossModel.Stats> =
        bodyOrFailure(
            client.tryToGet(url("/stats")) {
                if (!shouldHavePostLogin()) {
                    parameter("username", appSettingsService.getUserName())
                    parameter("password", appSettingsService.getPassword())
                }
                if (appSettingsService.getBasicUserName()
                        .isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()
                ) {
                    headers {
                        append(
                            HttpHeaders.Authorization,
                            constructBasicAuthValue(
                                BasicAuthCredentials(
                                    username = appSettingsService.getBasicUserName(),
                                    password = appSettingsService.getBasicPassword(),
                                ),
                            ),
                        )
                    }
                }
            },
        )

    suspend fun tags(): StatusAndData<List<SelfossModel.Tag>> =
        bodyOrFailure(
            client.tryToGet(url("/tags")) {
                if (!shouldHavePostLogin()) {
                    parameter("username", appSettingsService.getUserName())
                    parameter("password", appSettingsService.getPassword())
                }
                if (appSettingsService.getBasicUserName()
                        .isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()
                ) {
                    headers {
                        append(
                            HttpHeaders.Authorization,
                            constructBasicAuthValue(
                                BasicAuthCredentials(
                                    username = appSettingsService.getBasicUserName(),
                                    password = appSettingsService.getBasicPassword(),
                                ),
                            ),
                        )
                    }
                }
            },
        )

    suspend fun update(): StatusAndData<String> =
        bodyOrFailure(
            client.tryToGet(url("/update")) {
                if (!shouldHavePostLogin()) {
                    parameter("username", appSettingsService.getUserName())
                    parameter("password", appSettingsService.getPassword())
                }
                if (appSettingsService.getBasicUserName()
                        .isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()
                ) {
                    headers {
                        append(
                            HttpHeaders.Authorization,
                            constructBasicAuthValue(
                                BasicAuthCredentials(
                                    username = appSettingsService.getBasicUserName(),
                                    password = appSettingsService.getBasicPassword(),
                                ),
                            ),
                        )
                    }
                }
            },
        )

    suspend fun spouts(): StatusAndData<Map<String, SelfossModel.Spout>> =
        bodyOrFailure(
            client.tryToGet(url("/sources/spouts")) {
                if (!shouldHavePostLogin()) {
                    parameter("username", appSettingsService.getUserName())
                    parameter("password", appSettingsService.getPassword())
                }
                if (appSettingsService.getBasicUserName()
                        .isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()
                ) {
                    headers {
                        append(
                            HttpHeaders.Authorization,
                            constructBasicAuthValue(
                                BasicAuthCredentials(
                                    username = appSettingsService.getBasicUserName(),
                                    password = appSettingsService.getBasicPassword(),
                                ),
                            ),
                        )
                    }
                }
            },
        )

    suspend fun sourcesStats(): StatusAndData<ArrayList<SelfossModel.SourceStats>> =
        bodyOrFailure(
            client.tryToGet(url("/sources/stats")) {
                if (!shouldHavePostLogin()) {
                    parameter("username", appSettingsService.getUserName())
                    parameter("password", appSettingsService.getPassword())
                }
                if (appSettingsService.getBasicUserName()
                        .isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()
                ) {
                    headers {
                        append(
                            HttpHeaders.Authorization,
                            constructBasicAuthValue(
                                BasicAuthCredentials(
                                    username = appSettingsService.getBasicUserName(),
                                    password = appSettingsService.getBasicPassword(),
                                ),
                            ),
                        )
                    }
                }
            },
        )

    suspend fun sourcesDetailed(): StatusAndData<ArrayList<SelfossModel.SourceDetail>> =
        bodyOrFailure(
            client.tryToGet(url("/sources/list")) {
                if (!shouldHavePostLogin()) {
                    parameter("username", appSettingsService.getUserName())
                    parameter("password", appSettingsService.getPassword())
                }
                if (appSettingsService.getBasicUserName()
                        .isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()
                ) {
                    headers {
                        append(
                            HttpHeaders.Authorization,
                            constructBasicAuthValue(
                                BasicAuthCredentials(
                                    username = appSettingsService.getBasicUserName(),
                                    password = appSettingsService.getBasicPassword(),
                                ),
                            ),
                        )
                    }
                }
            },
        )

    suspend fun apiInformation(): StatusAndData<SelfossModel.ApiInformation> =
        bodyOrFailure(
            client.tryToGet(url("/api/about")) {
                if (appSettingsService.getBasicUserName()
                        .isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()
                ) {
                    headers {
                        append(
                            HttpHeaders.Authorization,
                            constructBasicAuthValue(
                                BasicAuthCredentials(
                                    username = appSettingsService.getBasicUserName(),
                                    password = appSettingsService.getBasicPassword(),
                                ),
                            ),
                        )
                    }
                }
            },
        )

    suspend fun markAsRead(id: String): SuccessResponse =
        maybeResponse(
            client.tryToPost(url("/mark/$id")) {
                if (!shouldHavePostLogin()) {
                    parameter("username", appSettingsService.getUserName())
                    parameter("password", appSettingsService.getPassword())
                }
                if (appSettingsService.getBasicUserName()
                        .isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()
                ) {
                    headers {
                        append(
                            HttpHeaders.Authorization,
                            constructBasicAuthValue(
                                BasicAuthCredentials(
                                    username = appSettingsService.getBasicUserName(),
                                    password = appSettingsService.getBasicPassword(),
                                ),
                            ),
                        )
                    }
                }
            },
        )

    suspend fun unmarkAsRead(id: String): SuccessResponse =
        maybeResponse(
            client.tryToPost(url("/unmark/$id")) {
                if (!shouldHavePostLogin()) {
                    parameter("username", appSettingsService.getUserName())
                    parameter("password", appSettingsService.getPassword())
                }
                if (appSettingsService.getBasicUserName()
                        .isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()
                ) {
                    headers {
                        append(
                            HttpHeaders.Authorization,
                            constructBasicAuthValue(
                                BasicAuthCredentials(
                                    username = appSettingsService.getBasicUserName(),
                                    password = appSettingsService.getBasicPassword(),
                                ),
                            ),
                        )
                    }
                }
            },
        )

    suspend fun starr(id: String): SuccessResponse =
        maybeResponse(
            client.tryToPost(url("/starr/$id")) {
                if (!shouldHavePostLogin()) {
                    parameter("username", appSettingsService.getUserName())
                    parameter("password", appSettingsService.getPassword())
                }
                if (appSettingsService.getBasicUserName()
                        .isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()
                ) {
                    headers {
                        append(
                            HttpHeaders.Authorization,
                            constructBasicAuthValue(
                                BasicAuthCredentials(
                                    username = appSettingsService.getBasicUserName(),
                                    password = appSettingsService.getBasicPassword(),
                                ),
                            ),
                        )
                    }
                }
            },
        )

    suspend fun unstarr(id: String): SuccessResponse =
        maybeResponse(
            client.tryToPost(url("/unstarr/$id")) {
                if (!shouldHavePostLogin()) {
                    parameter("username", appSettingsService.getUserName())
                    parameter("password", appSettingsService.getPassword())
                }
                if (appSettingsService.getBasicUserName()
                        .isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()
                ) {
                    headers {
                        append(
                            HttpHeaders.Authorization,
                            constructBasicAuthValue(
                                BasicAuthCredentials(
                                    username = appSettingsService.getBasicUserName(),
                                    password = appSettingsService.getBasicPassword(),
                                ),
                            ),
                        )
                    }
                }
            },
        )

    suspend fun markAllAsRead(ids: List<String>): SuccessResponse =
        maybeResponse(
            client.tryToSubmitForm(
                url = url("/mark"),
                formParameters =
                Parameters.build {
                    if (!shouldHavePostLogin()) {
                        append("username", appSettingsService.getUserName())
                        append("password", appSettingsService.getPassword())
                    }
                    ids.map { append("ids[]", it) }
                },
                block = {
                    if (appSettingsService.getBasicUserName()
                            .isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()
                    ) {
                        headers {
                            append(
                                HttpHeaders.Authorization,
                                constructBasicAuthValue(
                                    BasicAuthCredentials(
                                        username = appSettingsService.getBasicUserName(),
                                        password = appSettingsService.getBasicPassword(),
                                    ),
                                ),
                            )
                        }
                    }
                },
            ),
        )

    suspend fun createSourceForVersion(
        title: String,
        url: String,
        spout: String,
        tags: String,
    ): SuccessResponse =
        maybeResponse(
            if (appSettingsService.getApiVersion() > 1) {
                createSource("tags[]", title, url, spout, tags)
            } else {
                createSource("tags", title, url, spout, tags)
            },
        )

    private suspend fun createSource(
        tagsParamName: String,
        title: String,
        url: String,
        spout: String,
        tags: String,
    ): HttpResponse? =
        client.tryToSubmitForm(
            url = url("/source"),
            formParameters =
            Parameters.build {
                if (!shouldHavePostLogin()) {
                    append("username", appSettingsService.getUserName())
                    append("password", appSettingsService.getPassword())
                }
                append("title", title)
                append("url", url)
                append("spout", spout)
                append(tagsParamName, tags)
            },
            block = {
                if (appSettingsService.getBasicUserName()
                        .isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()
                ) {
                    headers {
                        append(
                            HttpHeaders.Authorization,
                            constructBasicAuthValue(
                                BasicAuthCredentials(
                                    username = appSettingsService.getBasicUserName(),
                                    password = appSettingsService.getBasicPassword(),
                                ),
                            ),
                        )
                    }
                }
            },
        )

    suspend fun updateSourceForVersion(
        id: Int,
        title: String,
        url: String,
        spout: String,
        tags: String,
    ): SuccessResponse =
        maybeResponse(
            if (appSettingsService.getApiVersion() > 1) {
                updateSource(id, "tags[]", title, url, spout, tags)
            } else {
                updateSource(id, "tags", title, url, spout, tags)
            },
        )

    private suspend fun updateSource(
        id: Int,
        tagsParamName: String,
        title: String,
        url: String,
        spout: String,
        tags: String,
    ): HttpResponse? =
        client.tryToSubmitForm(
            url = url("/source/$id"),
            formParameters =
            Parameters.build {
                if (!shouldHavePostLogin()) {
                    append("username", appSettingsService.getUserName())
                    append("password", appSettingsService.getPassword())
                }
                append("title", title)
                append("url", url)
                append("spout", spout)
                append(tagsParamName, tags)
            },
            block = {
                if (appSettingsService.getBasicUserName()
                        .isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()
                ) {
                    headers {
                        append(
                            HttpHeaders.Authorization,
                            constructBasicAuthValue(
                                BasicAuthCredentials(
                                    username = appSettingsService.getBasicUserName(),
                                    password = appSettingsService.getBasicPassword(),
                                ),
                            ),
                        )
                    }
                }
            },
        )

    suspend fun deleteSource(id: Int): SuccessResponse =
        maybeResponse(
            client.tryToDelete(url("/source/$id")) {
                if (!shouldHavePostLogin()) {
                    parameter("username", appSettingsService.getUserName())
                    parameter("password", appSettingsService.getPassword())
                }
                if (appSettingsService.getBasicUserName()
                        .isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()
                ) {
                    headers {
                        append(
                            HttpHeaders.Authorization,
                            constructBasicAuthValue(
                                BasicAuthCredentials(
                                    username = appSettingsService.getBasicUserName(),
                                    password = appSettingsService.getBasicPassword(),
                                ),
                            ),
                        )
                    }
                }
            },
        )
}