package com.rosetta.messenger.data

import android.content.Context
import android.util.Log
import com.rosetta.messenger.crypto.CryptoManager
import com.rosetta.messenger.database.GroupEntity
import com.rosetta.messenger.database.MessageEntity
import com.rosetta.messenger.database.RosettaDatabase
import com.rosetta.messenger.network.GroupStatus
import com.rosetta.messenger.network.Packet
import com.rosetta.messenger.network.PacketCreateGroup
import com.rosetta.messenger.network.PacketGroupBan
import com.rosetta.messenger.network.PacketGroupInfo
import com.rosetta.messenger.network.PacketGroupInviteInfo
import com.rosetta.messenger.network.PacketGroupJoin
import com.rosetta.messenger.network.PacketGroupLeave
import com.rosetta.messenger.network.ProtocolClient
import dagger.hilt.android.qualifiers.ApplicationContext
import java.security.SecureRandom
import java.util.UUID
import java.util.concurrent.ConcurrentHashMap
import javax.inject.Inject
import javax.inject.Singleton
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withTimeoutOrNull
import kotlin.coroutines.resume

@Singleton
class GroupRepository @Inject constructor(
    @ApplicationContext context: Context,
    private val messageRepository: MessageRepository,
    private val protocolClient: ProtocolClient
) {

    private val db = RosettaDatabase.getDatabase(context.applicationContext)
    private val groupDao = db.groupDao()
    private val messageDao = db.messageDao()
    private val dialogDao = db.dialogDao()

    private val inviteInfoCache = ConcurrentHashMap<String, GroupInviteInfoResult>()

    companion object {
        private const val TAG = "GroupRepository"
        private const val GROUP_PREFIX = "#group:"
        private const val GROUP_INVITE_PASSWORD = "rosetta_group"
        private const val GROUP_WAIT_TIMEOUT_MS = 15_000L
        private const val GROUP_CREATED_MARKER = "\$a=Group created"
    }

    data class ParsedGroupInvite(
        val groupId: String,
        val title: String,
        val encryptKey: String,
        val description: String
    )

    data class GroupJoinResult(
        val success: Boolean,
        val dialogPublicKey: String? = null,
        val title: String = "",
        val status: GroupStatus = GroupStatus.NOT_JOINED,
        val error: String? = null
    )

    data class GroupInviteInfoResult(
        val groupId: String,
        val membersCount: Int,
        val status: GroupStatus
    )

    fun isGroupKey(value: String): Boolean {
        val normalized = value.trim().lowercase()
        return normalized.startsWith(GROUP_PREFIX) || normalized.startsWith("group:")
    }

    fun normalizeGroupId(value: String): String {
        val trimmed = value.trim()
        return when {
            trimmed.startsWith(GROUP_PREFIX) -> trimmed.removePrefix(GROUP_PREFIX).trim()
            trimmed.startsWith("group:", ignoreCase = true) ->
                trimmed.substringAfter(':').trim()
            else -> trimmed
        }
    }

    fun toGroupDialogPublicKey(groupId: String): String = "$GROUP_PREFIX${normalizeGroupId(groupId)}"

    fun constructInviteString(
        groupId: String,
        title: String,
        encryptKey: String,
        description: String = ""
    ): String {
        val normalizedGroupId = normalizeGroupId(groupId)
        if (normalizedGroupId.isBlank() || title.isBlank() || encryptKey.isBlank()) {
            return ""
        }
        val payload = buildString {
            append(normalizedGroupId)
            append(':')
            append(title)
            append(':')
            append(encryptKey)
            if (description.isNotBlank()) {
                append(':')
                append(description)
            }
        }
        val encoded = CryptoManager.encryptWithPassword(payload, GROUP_INVITE_PASSWORD)
        return "$GROUP_PREFIX$encoded"
    }

    fun parseInviteString(inviteString: String): ParsedGroupInvite? {
        if (!inviteString.trim().startsWith(GROUP_PREFIX)) return null
        val encodedPayload = inviteString.trim().removePrefix(GROUP_PREFIX)
        if (encodedPayload.isBlank()) return null

        val decodedPayload =
            CryptoManager.decryptWithPassword(encodedPayload, GROUP_INVITE_PASSWORD) ?: return null
        val parts = decodedPayload.split(':')
        if (parts.size < 3) return null

        return ParsedGroupInvite(
            groupId = normalizeGroupId(parts[0]),
            title = parts[1],
            encryptKey = parts[2],
            description = parts.drop(3).joinToString(":")
        )
    }

    suspend fun getGroupKey(
        accountPublicKey: String,
        accountPrivateKey: String,
        groupPublicKeyOrId: String
    ): String? {
        val groupId = normalizeGroupId(groupPublicKeyOrId)
        if (groupId.isBlank()) return null
        val stored = groupDao.getGroup(accountPublicKey, groupId) ?: return null
        return CryptoManager.decryptWithPassword(stored.key, accountPrivateKey)
    }

    suspend fun getGroup(accountPublicKey: String, groupPublicKeyOrId: String): GroupEntity? {
        val groupId = normalizeGroupId(groupPublicKeyOrId)
        if (groupId.isBlank()) return null
        return groupDao.getGroup(accountPublicKey, groupId)
    }

    suspend fun requestGroupMembers(groupPublicKeyOrId: String): List<String>? {
        val groupId = normalizeGroupId(groupPublicKeyOrId)
        if (groupId.isBlank()) return null

        val packet = PacketGroupInfo().apply {
            this.groupId = groupId
            this.members = emptyList()
        }
        protocolClient.send(packet)

        val response = awaitPacketOnce<PacketGroupInfo>(
            packetId = 0x12,
            timeoutMs = GROUP_WAIT_TIMEOUT_MS
        ) { incoming -> normalizeGroupId(incoming.groupId) == groupId }
            ?: return null

        return response.members
    }

    fun getCachedInviteInfo(groupId: String): GroupInviteInfoResult? {
        val normalized = normalizeGroupId(groupId)
        return inviteInfoCache[normalized]
    }

    fun cacheInviteInfo(groupId: String, status: GroupStatus, membersCount: Int) {
        val normalized = normalizeGroupId(groupId)
        inviteInfoCache[normalized] = GroupInviteInfoResult(
            groupId = normalized,
            membersCount = membersCount,
            status = status
        )
    }

    suspend fun requestInviteInfo(groupPublicKeyOrId: String): GroupInviteInfoResult? {
        val groupId = normalizeGroupId(groupPublicKeyOrId)
        if (groupId.isBlank()) return null

        val packet = PacketGroupInviteInfo().apply {
            this.groupId = groupId
            this.membersCount = 0
            this.groupStatus = GroupStatus.NOT_JOINED
        }
        protocolClient.send(packet)

        val response = awaitPacketOnce<PacketGroupInviteInfo>(
            packetId = 0x13,
            timeoutMs = GROUP_WAIT_TIMEOUT_MS
        ) { incoming -> normalizeGroupId(incoming.groupId) == groupId }
            ?: return null

        val result = GroupInviteInfoResult(
            groupId = groupId,
            membersCount = response.membersCount.coerceAtLeast(0),
            status = response.groupStatus
        )
        inviteInfoCache[groupId] = result
        return result
    }

    suspend fun createGroup(
        accountPublicKey: String,
        accountPrivateKey: String,
        title: String,
        description: String = ""
    ): GroupJoinResult {
        if (title.isBlank()) {
            return GroupJoinResult(success = false, error = "Title is empty")
        }

        val createPacket = PacketCreateGroup()
        protocolClient.send(createPacket)

        val response = awaitPacketOnce<PacketCreateGroup>(
            packetId = 0x11,
            timeoutMs = GROUP_WAIT_TIMEOUT_MS
        ) { packet -> packet.groupId.isNotBlank() }
            ?: return GroupJoinResult(success = false, error = "Create group timeout")

        val groupId = normalizeGroupId(response.groupId)
        if (groupId.isBlank()) {
            return GroupJoinResult(success = false, error = "Server returned empty group id")
        }

        val groupKey = generateGroupKey()
        val invite = constructInviteString(groupId, title.trim(), groupKey, description.trim())
        if (invite.isBlank()) {
            return GroupJoinResult(success = false, error = "Failed to construct invite")
        }

        val joinResult = joinGroup(accountPublicKey, accountPrivateKey, invite)

        if (joinResult.success) {
            val dialogPublicKey = joinResult.dialogPublicKey
            if (!dialogPublicKey.isNullOrBlank()) {
                emitGroupCreatedMarker(
                    accountPublicKey = accountPublicKey,
                    accountPrivateKey = accountPrivateKey,
                    dialogPublicKey = dialogPublicKey
                )
            }
        }

        return joinResult
    }

    suspend fun joinGroup(
        accountPublicKey: String,
        accountPrivateKey: String,
        inviteString: String
    ): GroupJoinResult {
        val parsed = parseInviteString(inviteString)
            ?: return GroupJoinResult(success = false, error = "Invalid invite string")

        val encodedGroupStringForServer =
            CryptoManager.encryptWithPassword(inviteString, accountPrivateKey)

        val packet = PacketGroupJoin().apply {
            groupId = parsed.groupId
            groupString = encodedGroupStringForServer
            groupStatus = GroupStatus.NOT_JOINED
        }
        protocolClient.send(packet)

        val response = awaitPacketOnce<PacketGroupJoin>(
            packetId = 0x14,
            timeoutMs = GROUP_WAIT_TIMEOUT_MS
        ) { incoming -> normalizeGroupId(incoming.groupId) == parsed.groupId }
            ?: return GroupJoinResult(success = false, error = "Join group timeout")

        if (response.groupStatus != GroupStatus.JOINED) {
            return GroupJoinResult(
                success = false,
                status = response.groupStatus,
                title = parsed.title,
                dialogPublicKey = toGroupDialogPublicKey(parsed.groupId),
                error = "Join rejected"
            )
        }

        persistJoinedGroup(
            accountPublicKey = accountPublicKey,
            accountPrivateKey = accountPrivateKey,
            parsedInvite = parsed,
            emitSystemJoinMessage = true
        )

        return GroupJoinResult(
            success = true,
            status = GroupStatus.JOINED,
            dialogPublicKey = toGroupDialogPublicKey(parsed.groupId),
            title = parsed.title
        )
    }

    /**
     * Desktop parity fix:
     * if user is already joined on server, repeated invite click should still restore local group key.
     */
    suspend fun ensureLocalGroupFromInvite(
        accountPublicKey: String,
        accountPrivateKey: String,
        inviteString: String
    ): GroupJoinResult {
        val parsed = parseInviteString(inviteString)
            ?: return GroupJoinResult(
                success = false,
                status = GroupStatus.INVALID,
                error = "Invalid invite string"
            )

        val existingGroupKey = getGroupKey(accountPublicKey, accountPrivateKey, parsed.groupId)
        if (!existingGroupKey.isNullOrBlank()) {
            return GroupJoinResult(
                success = true,
                status = GroupStatus.JOINED,
                dialogPublicKey = toGroupDialogPublicKey(parsed.groupId),
                title = parsed.title
            )
        }

        persistJoinedGroup(
            accountPublicKey = accountPublicKey,
            accountPrivateKey = accountPrivateKey,
            parsedInvite = parsed,
            emitSystemJoinMessage = false
        )

        return GroupJoinResult(
            success = true,
            status = GroupStatus.JOINED,
            dialogPublicKey = toGroupDialogPublicKey(parsed.groupId),
            title = parsed.title
        )
    }

    suspend fun synchronizeJoinedGroup(
        accountPublicKey: String,
        accountPrivateKey: String,
        packet: PacketGroupJoin
    ): GroupJoinResult? {
        if (packet.groupStatus != GroupStatus.JOINED) return null
        if (packet.groupString.isBlank()) return null

        val decryptedInvite =
            CryptoManager.decryptWithPassword(packet.groupString, accountPrivateKey) ?: return null
        val parsed = parseInviteString(decryptedInvite) ?: return null

        persistJoinedGroup(
            accountPublicKey = accountPublicKey,
            accountPrivateKey = accountPrivateKey,
            parsedInvite = parsed,
            emitSystemJoinMessage = false
        )

        return GroupJoinResult(
            success = true,
            status = GroupStatus.JOINED,
            dialogPublicKey = toGroupDialogPublicKey(parsed.groupId),
            title = parsed.title
        )
    }

    suspend fun leaveGroup(accountPublicKey: String, groupPublicKeyOrId: String): Boolean {
        val groupId = normalizeGroupId(groupPublicKeyOrId)
        if (groupId.isBlank()) return false

        val packet = PacketGroupLeave().apply {
            this.groupId = groupId
        }
        protocolClient.send(packet)

        val response = awaitPacketOnce<PacketGroupLeave>(
            packetId = 0x15,
            timeoutMs = GROUP_WAIT_TIMEOUT_MS
        ) { incoming -> normalizeGroupId(incoming.groupId) == groupId }
            ?: return false

        if (normalizeGroupId(response.groupId) != groupId) return false

        val groupDialogKey = toGroupDialogPublicKey(groupId)
        groupDao.deleteGroup(accountPublicKey, groupId)
        messageDao.deleteDialog(accountPublicKey, groupDialogKey)
        dialogDao.deleteDialog(accountPublicKey, groupDialogKey)
        return true
    }

    suspend fun kickMember(groupPublicKeyOrId: String, memberPublicKey: String): Boolean {
        val groupId = normalizeGroupId(groupPublicKeyOrId)
        val targetPublicKey = memberPublicKey.trim()
        if (groupId.isBlank() || targetPublicKey.isBlank()) return false

        val packet = PacketGroupBan().apply {
            this.groupId = groupId
            this.publicKey = targetPublicKey
        }
        protocolClient.send(packet)

        val response = awaitPacketOnce<PacketGroupBan>(
            packetId = 0x16,
            timeoutMs = GROUP_WAIT_TIMEOUT_MS
        ) { incoming ->
            normalizeGroupId(incoming.groupId) == groupId &&
                incoming.publicKey.trim().equals(targetPublicKey, ignoreCase = true)
        } ?: return false

        return normalizeGroupId(response.groupId) == groupId &&
            response.publicKey.trim().equals(targetPublicKey, ignoreCase = true)
    }

    private suspend fun persistJoinedGroup(
        accountPublicKey: String,
        accountPrivateKey: String,
        parsedInvite: ParsedGroupInvite,
        emitSystemJoinMessage: Boolean
    ) {
        val encryptedGroupKey =
            CryptoManager.encryptWithPassword(parsedInvite.encryptKey, accountPrivateKey)

        groupDao.insertGroup(
            GroupEntity(
                account = accountPublicKey,
                groupId = parsedInvite.groupId,
                title = parsedInvite.title,
                description = parsedInvite.description,
                key = encryptedGroupKey
            )
        )

        val dialogPublicKey = toGroupDialogPublicKey(parsedInvite.groupId)

        if (emitSystemJoinMessage) {
            val joinText = "\$a=Group joined"
            val encryptedPlainMessage = CryptoManager.encryptWithPassword(joinText, accountPrivateKey)
            val encryptedContent = CryptoManager.encryptWithPassword(joinText, parsedInvite.encryptKey)

            messageDao.insertMessage(
                MessageEntity(
                    account = accountPublicKey,
                    fromPublicKey = accountPublicKey,
                    toPublicKey = dialogPublicKey,
                    content = encryptedContent,
                    timestamp = System.currentTimeMillis(),
                    chachaKey = buildStoredGroupKey(parsedInvite.encryptKey, accountPrivateKey),
                    read = 1,
                    fromMe = 1,
                    delivered = 1,
                    messageId = UUID.randomUUID().toString().replace("-", "").take(32),
                    plainMessage = encryptedPlainMessage,
                    attachments = "[]",
                    primaryAttachmentType = -1,
                    dialogKey = dialogPublicKey
                )
            )

            dialogDao.updateDialogFromMessages(accountPublicKey, dialogPublicKey)
        }

        // Ensure title is present after updateDialogFromMessages for list/header rendering.
        dialogDao.updateOpponentDisplayName(
            accountPublicKey,
            dialogPublicKey,
            parsedInvite.title,
            parsedInvite.description
        )
    }

    private suspend fun emitGroupCreatedMarker(
        accountPublicKey: String,
        accountPrivateKey: String,
        dialogPublicKey: String
    ) {
        try {
            messageRepository.initialize(accountPublicKey, accountPrivateKey)
            messageRepository.sendMessage(
                toPublicKey = dialogPublicKey,
                text = GROUP_CREATED_MARKER
            )
        } catch (e: Exception) {
            Log.w(TAG, "Failed to emit group-created marker for sync visibility", e)
        }
    }

    private fun buildStoredGroupKey(groupKey: String, privateKey: String): String {
        val encrypted = CryptoManager.encryptWithPassword(groupKey, privateKey)
        return "group:$encrypted"
    }

    private fun generateGroupKey(): String {
        val bytes = ByteArray(32)
        SecureRandom().nextBytes(bytes)
        return bytes.joinToString("") { "%02x".format(it) }
    }

    private suspend inline fun <reified T : Packet> awaitPacketOnce(
        packetId: Int,
        timeoutMs: Long,
        crossinline predicate: (T) -> Boolean = { true }
    ): T? {
        return withTimeoutOrNull(timeoutMs) {
            suspendCancellableCoroutine { continuation ->
                lateinit var callback: (Packet) -> Unit
                callback = { packet ->
                    val typedPacket = packet as? T
                    if (typedPacket != null && predicate(typedPacket)) {
                        protocolClient.unwaitPacket(packetId, callback)
                        continuation.resume(typedPacket)
                    }
                }
                protocolClient.waitPacket(packetId, callback)
                continuation.invokeOnCancellation {
                    protocolClient.unwaitPacket(packetId, callback)
                }
            }
        }
    }
}
