/* Copyright 2022 Tusky contributors
 *
 * This file is a part of Tusky.
 *
 * This program is free software; you can redistribute it and/or modify it under the terms of the
 * GNU General Public License as published by the Free Software Foundation; either version 3 of the
 * License, or (at your option) any later version.
 *
 * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
 * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
 * Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along with Tusky; if not,
 * see <http://www.gnu.org/licenses>. */

package com.keylesspalace.tusky.components.instanceinfo

import android.util.Log
import at.connyduck.calladapter.networkresult.NetworkResult
import at.connyduck.calladapter.networkresult.fold
import at.connyduck.calladapter.networkresult.getOrElse
import at.connyduck.calladapter.networkresult.getOrThrow
import at.connyduck.calladapter.networkresult.map
import at.connyduck.calladapter.networkresult.onSuccess
import at.connyduck.calladapter.networkresult.recoverCatching
import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.db.AppDatabase
import com.keylesspalace.tusky.db.entity.EmojisEntity
import com.keylesspalace.tusky.db.entity.InstanceInfoEntity
import com.keylesspalace.tusky.di.ApplicationScope
import com.keylesspalace.tusky.entity.Emoji
import com.keylesspalace.tusky.entity.Instance
import com.keylesspalace.tusky.entity.InstanceV1
import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.util.isHttpNotFound
import java.util.concurrent.ConcurrentHashMap
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

class InstanceInfoRepository @Inject constructor(
    private val api: MastodonApi,
    db: AppDatabase,
    private val accountManager: AccountManager,
    @ApplicationScope
    private val externalScope: CoroutineScope
) {
    private val dao = db.instanceDao()
    private val instanceName
        get() = accountManager.activeAccount!!.domain

    fun precache() {
        // We are avoiding some duplicate work but we are not trying too hard.
        // We might request it multiple times in parallel which is not a big problem.
        // We might also get the results in random order or write them twice but it's also
        // not a problem.
        // We are just trying to avoid 2 things:
        //  - fetching it when we already have it
        //  - caching default value (we want to rather re-fetch if it fails)
        if (instanceInfoCache[instanceName] == null) {
            externalScope.launch {
                fetchAndPersistInstanceInfo().fold({ fetched ->
                    instanceInfoCache[instanceName] = fetched.toInfoOrDefault()
                }, { e ->
                    Log.w(TAG, "failed to precache instance info", e)
                })
            }
        }
    }

    val cachedInstanceInfoOrFallback: InstanceInfo
        get() = instanceInfoCache[instanceName] ?: null.toInfoOrDefault()

    /**
     * Returns the custom emojis of the instance.
     * Will always try to fetch them from the api, falls back to cached Emojis in case it is not available.
     * Never throws, returns empty list in case of error.
     */
    suspend fun getEmojis(): List<Emoji> = withContext(Dispatchers.IO) {
        api.getCustomEmojis()
            .onSuccess { emojiList -> dao.upsert(EmojisEntity(instanceName, emojiList)) }
            .getOrElse { throwable ->
                Log.w(TAG, "failed to load custom emojis, falling back to cache", throwable)
                dao.getEmojiInfo(instanceName)?.emojiList.orEmpty()
            }
    }

    /**
     * Returns information about the instance.
     * Will always try to fetch the most up-to-date data from the api, falls back to cache in case it is not available.
     * Never throws, returns defaults of vanilla Mastodon in case of error.
     */
    suspend fun getUpdatedInstanceInfoOrFallback(): InstanceInfo =
        withContext(Dispatchers.IO) {
            fetchAndPersistInstanceInfo()
                .getOrElse { throwable ->
                    Log.w(
                        TAG,
                        "failed to load instance, falling back to cache and default values",
                        throwable
                    )
                    dao.getInstanceInfo(instanceName)
                }
        }.toInfoOrDefault()

    suspend fun saveFilterV2Support(filterV2Supported: Boolean) = dao.setFilterV2Support(instanceName, filterV2Supported)

    suspend fun isFilterV2Supported(): Boolean = dao.getFilterV2Support(instanceName)

    private suspend fun InstanceInfoRepository.fetchAndPersistInstanceInfo(): NetworkResult<InstanceInfoEntity> =
        fetchRemoteInstanceInfo()
            .onSuccess { instanceInfoEntity ->
                dao.upsert(instanceInfoEntity)
            }

    private suspend fun fetchRemoteInstanceInfo(): NetworkResult<InstanceInfoEntity> {
        val instance = this.instanceName
        return api.getInstance()
            .map { it.toEntity() }
            .recoverCatching { t ->
                if (t.isHttpNotFound()) {
                    api.getInstanceV1().map { it.toEntity(instance) }.getOrThrow()
                } else {
                    throw t
                }
            }
    }

    private fun InstanceInfoEntity?.toInfoOrDefault() = InstanceInfo(
        maxChars = this?.maximumTootCharacters ?: DEFAULT_CHARACTER_LIMIT,
        pollMaxOptions = this?.maxPollOptions ?: DEFAULT_MAX_OPTION_COUNT,
        pollMaxLength = this?.maxPollOptionLength ?: DEFAULT_MAX_OPTION_LENGTH,
        pollMinDuration = this?.minPollDuration ?: DEFAULT_MIN_POLL_DURATION,
        pollMaxDuration = this?.maxPollDuration ?: DEFAULT_MAX_POLL_DURATION,
        charactersReservedPerUrl = this?.charactersReservedPerUrl
            ?: DEFAULT_CHARACTERS_RESERVED_PER_URL,
        videoSizeLimit = this?.videoSizeLimit ?: DEFAULT_VIDEO_SIZE_LIMIT,
        imageSizeLimit = this?.imageSizeLimit ?: DEFAULT_IMAGE_SIZE_LIMIT,
        imageMatrixLimit = this?.imageMatrixLimit ?: DEFAULT_IMAGE_MATRIX_LIMIT,
        maxMediaAttachments = this?.maxMediaAttachments
            ?: DEFAULT_MAX_MEDIA_ATTACHMENTS,
        maxFields = this?.maxFields ?: DEFAULT_MAX_ACCOUNT_FIELDS,
        maxFieldNameLength = this?.maxFieldNameLength,
        maxFieldValueLength = this?.maxFieldValueLength,
        version = this?.version,
        translationEnabled = this?.translationEnabled
    )

    private fun Instance.toEntity() = InstanceInfoEntity(
        instance = domain,
        maximumTootCharacters = this.configuration?.statuses?.maxCharacters
            ?: DEFAULT_CHARACTER_LIMIT,
        maxPollOptions = this.configuration?.polls?.maxOptions ?: DEFAULT_MAX_OPTION_COUNT,
        maxPollOptionLength = this.configuration?.polls?.maxCharactersPerOption
            ?: DEFAULT_MAX_OPTION_LENGTH,
        minPollDuration = this.configuration?.polls?.minExpirationSeconds
            ?: DEFAULT_MIN_POLL_DURATION,
        maxPollDuration = this.configuration?.polls?.maxExpirationSeconds
            ?: DEFAULT_MAX_POLL_DURATION,
        charactersReservedPerUrl = this.configuration?.statuses?.charactersReservedPerUrl
            ?: DEFAULT_CHARACTERS_RESERVED_PER_URL,
        version = this.version,
        videoSizeLimit = this.configuration?.mediaAttachments?.videoSizeLimitBytes?.toInt()
            ?: DEFAULT_VIDEO_SIZE_LIMIT,
        imageSizeLimit = this.configuration?.mediaAttachments?.imageSizeLimitBytes?.toInt()
            ?: DEFAULT_IMAGE_SIZE_LIMIT,
        imageMatrixLimit = this.configuration?.mediaAttachments?.imagePixelCountLimit?.toInt()
            ?: DEFAULT_IMAGE_MATRIX_LIMIT,
        maxMediaAttachments = this.configuration?.statuses?.maxMediaAttachments
            ?: DEFAULT_MAX_MEDIA_ATTACHMENTS,
        maxFields = this.pleroma?.metadata?.fieldLimits?.maxFields,
        maxFieldNameLength = this.pleroma?.metadata?.fieldLimits?.nameLength,
        maxFieldValueLength = this.pleroma?.metadata?.fieldLimits?.valueLength,
        translationEnabled = this.configuration?.translation?.enabled
    )

    private fun InstanceV1.toEntity(instanceName: String) =
        InstanceInfoEntity(
            instance = instanceName,
            maximumTootCharacters = this.configuration?.statuses?.maxCharacters
                ?: this.maxTootChars,
            maxPollOptions = this.configuration?.polls?.maxOptions
                ?: this.pollConfiguration?.maxOptions,
            maxPollOptionLength = this.configuration?.polls?.maxCharactersPerOption
                ?: this.pollConfiguration?.maxOptionChars,
            minPollDuration = this.configuration?.polls?.minExpiration
                ?: this.pollConfiguration?.minExpiration,
            maxPollDuration = this.configuration?.polls?.maxExpiration
                ?: this.pollConfiguration?.maxExpiration,
            charactersReservedPerUrl = this.configuration?.statuses?.charactersReservedPerUrl,
            version = this.version,
            videoSizeLimit = this.configuration?.mediaAttachments?.videoSizeLimit
                ?: this.uploadLimit,
            imageSizeLimit = this.configuration?.mediaAttachments?.imageSizeLimit
                ?: this.uploadLimit,
            imageMatrixLimit = this.configuration?.mediaAttachments?.imageMatrixLimit,
            maxMediaAttachments = this.configuration?.statuses?.maxMediaAttachments
                ?: this.maxMediaAttachments,
            maxFields = this.pleroma?.metadata?.fieldLimits?.maxFields,
            maxFieldNameLength = this.pleroma?.metadata?.fieldLimits?.nameLength,
            maxFieldValueLength = this.pleroma?.metadata?.fieldLimits?.valueLength,
            translationEnabled = null,
        )

    companion object {
        private const val TAG = "InstanceInfoRepo"

        /** In-memory cache for instance data, per instance domain.  */
        private var instanceInfoCache = ConcurrentHashMap<String, InstanceInfo>()

        const val DEFAULT_CHARACTER_LIMIT = 500
        private const val DEFAULT_MAX_OPTION_COUNT = 4
        private const val DEFAULT_MAX_OPTION_LENGTH = 50
        private const val DEFAULT_MIN_POLL_DURATION = 300
        private const val DEFAULT_MAX_POLL_DURATION = 604800

        private const val DEFAULT_VIDEO_SIZE_LIMIT = 41943040 // 40MiB
        private const val DEFAULT_IMAGE_SIZE_LIMIT = 10485760 // 10MiB
        private const val DEFAULT_IMAGE_MATRIX_LIMIT = 16777216 // 4096^2 Pixels

        // Mastodon only counts URLs as this long in terms of status character limits
        const val DEFAULT_CHARACTERS_RESERVED_PER_URL = 23

        const val DEFAULT_MAX_MEDIA_ATTACHMENTS = 4
        const val DEFAULT_MAX_ACCOUNT_FIELDS = 4
    }
}
