package org.thoughtcrime.securesms.groups

import android.content.Context
import androidx.annotation.WorkerThread
import okhttp3.HttpUrl
import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.open_groups.GroupMemberRole
import org.session.libsession.messaging.open_groups.OpenGroup
import org.session.libsession.messaging.open_groups.OpenGroupApi
import org.session.libsession.messaging.sending_receiving.pollers.OpenGroupPoller
import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.ThreadUtils
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
import java.util.concurrent.Executors

object OpenGroupManager {
    private val executorService = Executors.newScheduledThreadPool(4)
    private val pollers = mutableMapOf<String, OpenGroupPoller>() // One for each server
    private var isPolling = false
    private val pollUpdaterLock = Any()

    val isAllCaughtUp: Boolean
        get() {
            pollers.values.forEach { poller ->
                val jobID = poller.secondToLastJob?.id
                jobID?.let {
                    val storage = MessagingModuleConfiguration.shared.storage
                    if (storage.getMessageReceiveJob(jobID) == null) {
                        // If the second to last job is done, it means we are now handling the last job
                        poller.isCaughtUp = true
                        poller.secondToLastJob = null
                    }
                }
                if (!poller.isCaughtUp) { return false }
            }
            return true
        }

    fun startPolling() {
        if (isPolling) { return }
        isPolling = true
        val storage = MessagingModuleConfiguration.shared.storage
        val servers = storage.getAllOpenGroups().values.map { it.server }.toSet()
        synchronized(pollUpdaterLock) {
            servers.forEach { server ->
                pollers[server]?.stop() // Shouldn't be necessary
                pollers[server] = OpenGroupPoller(server, executorService).apply { startIfNeeded() }
            }
        }
    }

    fun stopPolling() {
        synchronized(pollUpdaterLock) {
            pollers.forEach { it.value.stop() }
            pollers.clear()
            isPolling = false
        }
    }

    @WorkerThread
    fun add(server: String, room: String, publicKey: String, context: Context): OpenGroupApi.RoomInfo? {
        val openGroupID = "$server.$room"
        val threadID = GroupManager.getOpenGroupThreadID(openGroupID, context)
        val storage = MessagingModuleConfiguration.shared.storage
        val threadDB = DatabaseComponent.get(context).lokiThreadDatabase()
        // Check it it's added already
        val existingOpenGroup = threadDB.getOpenGroupChat(threadID)
        if (existingOpenGroup != null) { return null }
        // Clear any existing data if needed
        storage.removeLastDeletionServerID(room, server)
        storage.removeLastMessageServerID(room, server)
        storage.removeLastInboxMessageId(server)
        storage.removeLastOutboxMessageId(server)
        // Store the public key
        storage.setOpenGroupPublicKey(server, publicKey)
        // Get capabilities & room info
        val (capabilities, info) = OpenGroupApi.getCapabilitiesAndRoomInfo(room, server).get()
        storage.setServerCapabilities(server, capabilities.capabilities)
        // Create the group locally if not available already
        if (threadID < 0) {
            GroupManager.createOpenGroup(openGroupID, context, null, info.name)
        }
        OpenGroupPoller.handleRoomPollInfo(
            server = server,
            roomToken = room,
            pollInfo = info.toPollInfo(),
            createGroupIfMissingWithPublicKey = publicKey
        )
        return info
    }

    fun restartPollerForServer(server: String) {
        // Start the poller if needed
        synchronized(pollUpdaterLock) {
            pollers[server]?.stop()
            pollers[server]?.startIfNeeded() ?: run {
                val poller = OpenGroupPoller(server, executorService)
                Log.d("Loki", "Starting poller for open group: $server")
                pollers[server] = poller
                poller.startIfNeeded()
            }
        }
    }

    fun delete(server: String, room: String, context: Context) {
        val storage = MessagingModuleConfiguration.shared.storage
        val threadDB = DatabaseComponent.get(context).threadDatabase()
        val openGroupID = "$server.$room"
        val threadID = GroupManager.getOpenGroupThreadID(openGroupID, context)
        val recipient = threadDB.getRecipientForThreadId(threadID) ?: return
        threadDB.setThreadArchived(threadID)
        val groupID = recipient.address.serialize()
        // Stop the poller if needed
        val openGroups = storage.getAllOpenGroups().filter { it.value.server == server }
        if (openGroups.count() == 1) {
            synchronized(pollUpdaterLock) {
                val poller = pollers[server]
                poller?.stop()
                pollers.remove(server)
            }
        }
        // Delete
        storage.removeLastDeletionServerID(room, server)
        storage.removeLastMessageServerID(room, server)
        storage.removeLastInboxMessageId(server)
        storage.removeLastOutboxMessageId(server)
        val lokiThreadDB = DatabaseComponent.get(context).lokiThreadDatabase()
        lokiThreadDB.removeOpenGroupChat(threadID)
        ThreadUtils.queue {
            threadDB.deleteConversation(threadID) // Must be invoked on a background thread
            GroupManager.deleteGroup(groupID, context) // Must be invoked on a background thread
        }
    }

    fun addOpenGroup(urlAsString: String, context: Context): OpenGroupApi.RoomInfo? {
        val url = HttpUrl.parse(urlAsString) ?: return null
        val server = OpenGroup.getServer(urlAsString)
        val room = url.pathSegments().firstOrNull() ?: return null
        val publicKey = url.queryParameter("public_key") ?: return null

        return add(server.toString().removeSuffix("/"), room, publicKey, context) // assume migrated from calling function
    }

    fun updateOpenGroup(openGroup: OpenGroup, context: Context) {
        val threadDB = DatabaseComponent.get(context).lokiThreadDatabase()
        val openGroupID = "${openGroup.server}.${openGroup.room}"
        val threadID = GroupManager.getOpenGroupThreadID(openGroupID, context)
        threadDB.setOpenGroupChat(openGroup, threadID)
    }

    fun isUserModerator(context: Context, groupId: String, standardPublicKey: String, blindedPublicKey: String? = null): Boolean {
        val memberDatabase = DatabaseComponent.get(context).groupMemberDatabase()
        val standardRoles = memberDatabase.getGroupMemberRoles(groupId, standardPublicKey)
        val blindedRoles = blindedPublicKey?.let { memberDatabase.getGroupMemberRoles(groupId, it) } ?: emptyList()

        // roles to check against
        val moderatorRoles = listOf(
            GroupMemberRole.MODERATOR, GroupMemberRole.ADMIN,
            GroupMemberRole.HIDDEN_MODERATOR, GroupMemberRole.HIDDEN_ADMIN
        )
        return standardRoles.any { it in moderatorRoles } || blindedRoles.any { it in moderatorRoles }
    }

}