package net.buzzert.kordophone.backend.db

import android.util.Log
import io.realm.kotlin.MutableRealm
import io.realm.kotlin.Realm
import io.realm.kotlin.RealmConfiguration
import io.realm.kotlin.UpdatePolicy
import io.realm.kotlin.ext.toRealmList
import io.realm.kotlin.query.find
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.map
import net.buzzert.kordophone.backend.db.model.Conversation
import net.buzzert.kordophone.backend.db.model.Message
import net.buzzert.kordophone.backend.db.model.toDatabaseConversation
import net.buzzert.kordophone.backend.db.model.toDatabaseMessage
import net.buzzert.kordophone.backend.db.model.toRealmInstant
import net.buzzert.kordophone.backend.model.GUID
import net.buzzert.kordophone.backend.server.REPO_LOG
import java.lang.IllegalArgumentException
import net.buzzert.kordophone.backend.model.Conversation as ModelConversation
import net.buzzert.kordophone.backend.model.Message as ModelMessage

class CachedChatDatabase (private val realmConfig: RealmConfiguration) {
    companion object {
        private val schema = setOf(Conversation::class, Message::class)

        fun liveDatabase(): CachedChatDatabase {
            return CachedChatDatabase(
                RealmConfiguration.Builder(schema = schema)
                    .name("chat-cache")
                    .build()
            )
        }

        fun testDatabase(): CachedChatDatabase {
            return CachedChatDatabase(
                RealmConfiguration.Builder(schema = schema)
                    .name("chat-cache-test")
                    .inMemory()
                    .build()
            )
        }
    }

    private val realm = runCatching {
        Realm.open(realmConfig)
    }.recover {
        // We're just a caching layer, so in the event of a migration error, just delete and start over.
        Log.d(REPO_LOG, "Error opening (${it.message}). Recovering by deleting database.")
        Realm.deleteRealm(realmConfig)

        return@recover Realm.open(realmConfig)
    }.getOrThrow()

    // Flow for watching changes to the database
    val conversationChanges: Flow<List<ModelConversation>>
        get() = realm.query(Conversation::class).find().asFlow().map {
            realm.copyFromRealm(it.list)
                .map { it.toConversation() }
        }

    // Flow for watching for message changes for a given conversation
    fun messagesChanged(conversation: ModelConversation): Flow<List<ModelMessage>> {
        return realm.query(Message::class, "conversationGUID == $0", conversation.guid)
            .find()
            .asFlow()
            .map { it.list.map { it.toMessage(conversation) } }
    }

    fun updateConversations(incomingConversations: List<ModelConversation>) = realm.writeBlocking {
        val incomingDatabaseConversations = incomingConversations.map { it.toDatabaseConversation() }

        var deletedConversations = realm.query(Conversation::class).find()
            .minus(incomingDatabaseConversations)

        deletedConversations.forEach { conversation ->
            findLatest(conversation)?.let {
                delete(it)
            }
        }

        writeManagedConversations(this, incomingDatabaseConversations)
    }

    fun writeConversations(conversations: List<ModelConversation>) = realm.writeBlocking {
        writeManagedConversations(this, conversations.map { it.toDatabaseConversation() })
    }

    private fun writeManagedConversations(mutableRealm: MutableRealm, conversations: List<Conversation>) {
        conversations.forEach {conversation ->
            try {
                val managedConversation = getManagedConversationByGuid(conversation.guid)
                mutableRealm.findLatest(managedConversation)?.apply {
                    displayName = conversation.displayName
                    participants = conversation.participants
                    date = conversation.date
                    unreadCount = conversation.unreadCount
                    lastMessagePreview = conversation.lastMessagePreview
                    lastMessageGUID = conversation.lastMessageGUID
                }
            } catch (e: NoSuchElementException) {
                // Conversation does not exist. Copy it to the realm.
                mutableRealm.copyToRealm(conversation, updatePolicy = UpdatePolicy.ALL)
            }
        }
    }

    fun deleteConversations(conversations: List<ModelConversation>) = realm.writeBlocking {
        conversations.forEach { inConversation ->
            val conversation = getManagedConversationByGuid(inConversation.guid)
            findLatest(conversation)?.let {
                delete(it)
            }
        }
    }

    fun fetchConversations(): List<ModelConversation> {
        val itemResults = realm.query(Conversation::class).find()
        val items = realm.copyFromRealm(itemResults)
        return items.map { it.toConversation() }
    }

    fun writeMessages(messages: List<ModelMessage>, conversation: ModelConversation, outgoing: Boolean = false) {
        if (messages.isEmpty()) {
            return
        }

        val dbConversation = getManagedConversationByGuid(conversation.guid)
        realm.writeBlocking {
            messages
                .map { it.toDatabaseMessage(outgoing = outgoing) }
                .map { copyToRealm(it, updatePolicy = UpdatePolicy.ALL) }

            findLatest(dbConversation)?.let {
                val lastMessage = messages.maxByOrNull { it.date }!!

                val lastMessageDate = lastMessage.date.toInstant().toRealmInstant()
                if (lastMessageDate > it.date) {
                    it.lastMessageGUID = lastMessage.guid
                    it.lastMessagePreview = lastMessage.displayText

                    // This will cause sort order to change. I think this ends
                    // up getting updated whenever we get conversation changes anyway.
                    // it.date = lastMessageDate
                }
            }
        }
    }

    fun fetchMessages(conversation: ModelConversation): List<ModelMessage> {
        return realm.query(Message::class, "conversationGUID == $0", conversation.guid)
            .find()
            .map { it.toMessage(conversation) }
    }

    fun close() {
        realm.close()
    }

    private fun getManagedConversationByGuid(guid: GUID): Conversation {
        return realm.query(Conversation::class, "guid == $0", guid)
            .find()
            .first()
    }

    fun getConversationByGuid(guid: GUID): Conversation {
        return realm.copyFromRealm(getManagedConversationByGuid(guid))
    }
}