package su.reya.coop

import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.request.get
import io.ktor.client.statement.HttpResponse
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.supervisorScope
import rust.nostr.sdk.AckPolicy
import rust.nostr.sdk.Alphabet
import rust.nostr.sdk.AsyncNostrSigner
import rust.nostr.sdk.Client
import rust.nostr.sdk.ClientBuilder
import rust.nostr.sdk.ClientNotification
import rust.nostr.sdk.Contact
import rust.nostr.sdk.Event
import rust.nostr.sdk.EventBuilder
import rust.nostr.sdk.EventId
import rust.nostr.sdk.Filter
import rust.nostr.sdk.GossipConfig
import rust.nostr.sdk.Keys
import rust.nostr.sdk.Kind
import rust.nostr.sdk.KindStandard
import rust.nostr.sdk.LogLevel
import rust.nostr.sdk.Metadata
import rust.nostr.sdk.MetadataRecord
import rust.nostr.sdk.Nip05Address
import rust.nostr.sdk.Nip05Profile
import rust.nostr.sdk.NostrDatabase
import rust.nostr.sdk.NostrGossip
import rust.nostr.sdk.PublicKey
import rust.nostr.sdk.RelayCapabilities
import rust.nostr.sdk.RelayMessageEnum
import rust.nostr.sdk.RelayMetadata
import rust.nostr.sdk.RelayStatus
import rust.nostr.sdk.RelayUrl
import rust.nostr.sdk.ReqExitPolicy
import rust.nostr.sdk.ReqTarget
import rust.nostr.sdk.SendEventTarget
import rust.nostr.sdk.SignerAuthenticator
import rust.nostr.sdk.SingleLetterTag
import rust.nostr.sdk.SleepWhenIdle
import rust.nostr.sdk.SubscribeAutoCloseOptions
import rust.nostr.sdk.Tag
import rust.nostr.sdk.Timestamp
import rust.nostr.sdk.UnsignedEvent
import rust.nostr.sdk.extractRelayList
import rust.nostr.sdk.initLogger
import rust.nostr.sdk.nip17ExtractRelayList
import rust.nostr.sdk.nip59MakeGiftWrapAsync
import kotlin.coroutines.cancellation.CancellationException
import kotlin.time.Duration

object NostrManager {
    val instance = Nostr()

    val BOOTSTRAP_RELAYS = listOf(
        "wss://relay.primal.net",
        "wss://purplepag.es"
    )

    val INDEXER_RELAY = listOf(
        "wss://indexer.coracle.social",
    )

    val ALL_RELAYS = BOOTSTRAP_RELAYS + INDEXER_RELAY
}

class Nostr {
    var client: Client? = null
        private set
    var signer: UniversalSigner = UniversalSigner(Keys.generate())
        private set
    var sentEvents: MutableMap<EventId, List<RelayUrl>> = mutableMapOf()
        private set
    var rumorMap: MutableMap<EventId, EventId> = mutableMapOf()
        private set

    private val isInitialized = MutableStateFlow(false)

    private val _newEvents = MutableSharedFlow<UnsignedEvent>(extraBufferCapacity = 100)
    val newEvents = _newEvents.asSharedFlow()

    private val _metadataUpdates =
        MutableSharedFlow<Pair<PublicKey, Metadata>>(extraBufferCapacity = 100)
    val metadataUpdates = _metadataUpdates.asSharedFlow()

    private val _contactListUpdates = MutableSharedFlow<List<PublicKey>>(extraBufferCapacity = 100)
    val contactListUpdates = _contactListUpdates.asSharedFlow()

    private val _messageSyncState = MutableStateFlow(MessageSyncState())
    val messageSyncState = _messageSyncState.asStateFlow()

    suspend fun emitNewEvent(event: UnsignedEvent) {
        _newEvents.emit(event)
    }

    suspend fun emitMetadataUpdate(pubkey: PublicKey, metadata: Metadata) {
        _metadataUpdates.emit(pubkey to metadata)
    }

    suspend fun emitContactListUpdate(contacts: List<PublicKey>) {
        _contactListUpdates.emit(contacts)
    }

    suspend fun init(
        dbPath: String,
        logLevel: LogLevel = LogLevel.WARN
    ) {
        try {
            if (isInitialized.value) return

            // Initialize the logger for nostr client
            initLogger(logLevel)

            // Initialize configurations for nostr client
            val lmdb = NostrDatabase.lmdb(dbPath)
            val gossip = NostrGossip.inMemory()
            val authenticator = SignerAuthenticator(signer)
            val idleTimeout = Duration.parse("5m")

            client =
                ClientBuilder()
                    .authenticator(authenticator)
                    .database(lmdb)
                    .gossip(gossip)
                    .gossipConfig(
                        GossipConfig()
                            .noBackgroundRefresh()
                            .fetchTimeout(Duration.parse("2s"))
                    )
                    .verifySubscriptions(false)
                    .sleepWhenIdle(SleepWhenIdle.Enabled(idleTimeout))
                    .build()

            isInitialized.value = true
        } catch (e: Exception) {
            throw IllegalStateException("Failed to initialize Nostr client: ${e.message}", e)
        }
    }

    suspend fun waitUntilInitialized() {
        isInitialized.first { it }
    }

    suspend fun connectBootstrapRelays() {
        NostrManager.BOOTSTRAP_RELAYS.forEach { url ->
            client?.addRelay(RelayUrl.parse(url))
        }
        NostrManager.INDEXER_RELAY.forEach { url ->
            client?.addRelay(
                url = RelayUrl.parse(url),
                capabilities = RelayCapabilities.gossip()
            )
        }
        // Connect to all bootstrap relays
        client?.connect()
    }

    suspend fun reconnect() {
        NostrManager.ALL_RELAYS.forEach { url ->
            try {
                client?.relay(RelayUrl.parse(url)).let { relay ->
                    if (relay != null) {
                        if (relay.status() != RelayStatus.CONNECTED) {
                            relay.connect()
                        }
                    }
                }
            } catch (e: Exception) {
                println("Failed to reconnect relay: ${e.message}")
            }
        }
    }

    suspend fun disconnect() {
        NostrManager.ALL_RELAYS.forEach { url ->
            try {
                client?.disconnectRelay(RelayUrl.parse(url))
            } catch (e: Exception) {
                println("Failed to disconnect relay: ${e.message}")
            }
        }
    }

    suspend fun prune() {
        try {
            client?.database()?.wipe()
        } catch (e: Exception) {
            throw IllegalStateException("Failed to prune database: ${e.message}", e)
        }
    }

    suspend fun setSigner(new: AsyncNostrSigner) {
        try {
            signer.switch(new)
        } catch (e: Exception) {
            throw IllegalStateException("Failed to set signer: ${e.message}", e)
        }
    }

    fun isSignedByUser(event: Event): Boolean {
        return try {
            signer.currentUser == event.author()
        } catch (e: Exception) {
            println("Failed to check if event is signed by user: ${e.message}")
            false
        }
    }

    suspend fun getUserMetadata() {
        try {
            val author = signer.currentUser ?: throw IllegalStateException("User not signed in")

            // Get the latest metadata event
            val metadataFilter =
                Filter().kind(Kind.fromStd(KindStandard.METADATA)).author(author).limit(1u)

            // Get the latest contact list event
            val contactFilter =
                Filter().kind(Kind.fromStd(KindStandard.CONTACT_LIST)).author(author).limit(1u)

            // Get the latest messaging relay list event
            val msgRelayFilter =
                Filter().kind(Kind.fromStd(KindStandard.INBOX_RELAYS)).author(author).limit(1u)

            // Construct a target that includes all filters
            val target = ReqTarget.auto(listOf(metadataFilter, contactFilter, msgRelayFilter))
            val opts = SubscribeAutoCloseOptions().exitPolicy(ReqExitPolicy.ExitOnEose)

            client?.subscribe(target = target, id = "user-metadata", closeOn = opts)
        } catch (e: Exception) {
            throw IllegalStateException("Failed to fetch user metadata: ${e.message}", e)
        }
    }

    suspend fun getUserMessages(msgRelayList: Event) {
        try {
            val author = signer.currentUser ?: throw IllegalStateException("User not signed in")
            val relays = nip17ExtractRelayList(msgRelayList)

            // Ensure relay connections
            relays.forEach { relay ->
                client?.addRelay(relay)
                client?.connectRelay(relay)
            }

            // Construct a filter for gift wrap events
            val filter = Filter().kind(Kind.fromStd(KindStandard.GIFT_WRAP)).pubkey(author)
            val target = mutableMapOf<RelayUrl, List<Filter>>()
            relays.forEach { relay ->
                target[relay] = listOf(filter)
            }

            client?.subscribe(
                target = ReqTarget.manual(target),
                id = "gift-wraps"
            )
        } catch (e: Exception) {
            throw IllegalStateException("Failed to fetch user messages: ${e.message}", e)
        }
    }

    @OptIn(ExperimentalCoroutinesApi::class)
    suspend fun handleNotifications(
        onMetadataUpdate: (PublicKey, Metadata) -> Unit,
        onContactListUpdate: (List<PublicKey>) -> Unit,
        onNewMessage: (UnsignedEvent) -> Unit,
    ) = supervisorScope {
        val now = Timestamp.now()
        val processedEvent = mutableSetOf<EventId>()
        val notifications = client?.notifications() ?: return@supervisorScope

        val giftWrapQueue = Channel<Event>(Channel.UNLIMITED)
        var processedCount = 0
        var eoseReceived = false

        launch(Dispatchers.Default) {
            for (event in giftWrapQueue) {
                val rumor = extractRumor(event) ?: continue
                processedCount++

                // Trigger new message notification
                if (rumor.createdAt().asSecs() >= now.asSecs()) {
                    onNewMessage(rumor)
                }

                // Update sync state
                _messageSyncState.update {
                    it.copy(
                        processedCount = processedCount,
                        isSyncing = !eoseReceived || !giftWrapQueue.isEmpty
                    )
                }
            }
        }

        while (true) {
            val notification = notifications.next() ?: continue

            when (notification) {
                is ClientNotification.Message -> {
                    val relayUrl = notification.relayUrl

                    when (val message = notification.message.asEnum()) {
                        is RelayMessageEnum.EventMsg -> {
                            val event = message.event

                            // Prevent processing duplicate events
                            if (processedEvent.contains(event.id())) continue
                            processedEvent.add(event.id())

                            when (event.kind().asStd()) {
                                KindStandard.METADATA -> {
                                    try {
                                        val metadata = Metadata.fromJson(event.content())
                                        onMetadataUpdate(event.author(), metadata)
                                    } catch (e: Exception) {
                                        println("Failed to parse metadata: $e")
                                    }
                                }

                                KindStandard.CONTACT_LIST -> {
                                    if (isSignedByUser(event = event)) {
                                        val pubkeys = event.tags().publicKeys()
                                        // Get mutual contacts
                                        getMutualContacts(pubkeys)
                                        // Emit contact list update
                                        onContactListUpdate(pubkeys)
                                    }
                                }

                                KindStandard.INBOX_RELAYS -> {
                                    // Get all gift wrap events for the current user
                                    if (isSignedByUser(event = event)) {
                                        getUserMessages(msgRelayList = event)
                                    }
                                }

                                KindStandard.GIFT_WRAP -> {
                                    giftWrapQueue.send(event)
                                }

                                else -> {}
                            }
                        }

                        is RelayMessageEnum.EndOfStoredEvents -> {
                            if (message.subscriptionId == "gift-wraps") {
                                eoseReceived = true
                                if (giftWrapQueue.isEmpty) {
                                    _messageSyncState.update { it.copy(isSyncing = false) }
                                }
                            }
                        }

                        is RelayMessageEnum.Ok -> {
                            if (sentEvents.containsKey(message.eventId)) {
                                val currentRelays = sentEvents[message.eventId] ?: emptyList()
                                sentEvents[message.eventId] = currentRelays + relayUrl
                            }
                        }

                        else -> {
                            /* Ignore other message types */
                        }
                    }
                }

                is ClientNotification.Shutdown -> {
                    break
                }

                else -> {
                    /* Ignore other message types */
                }
            }
        }
    }

    private suspend fun getMutualContacts(pubkeys: List<PublicKey>) {
        try {
            val kind = Kind.fromStd(KindStandard.CONTACT_LIST)
            val filter = Filter().kind(kind).authors(pubkeys).limit(200u)
            val opts = SubscribeAutoCloseOptions().exitPolicy(ReqExitPolicy.ExitOnEose)

            val target = mutableMapOf<RelayUrl, List<Filter>>()
            NostrManager.BOOTSTRAP_RELAYS.forEach { relay ->
                target[RelayUrl.parse(relay)] = listOf(filter)
            }

            client?.subscribe(
                target = ReqTarget.manual(target),
                id = "mutual-contacts",
                closeOn = opts,
            )
        } catch (e: Exception) {
            throw IllegalStateException("Failed to fetch mutual contacts: ${e.message}", e)
        }
    }

    private suspend fun getCachedRumor(giftId: EventId): UnsignedEvent? {
        try {
            val filter = Filter().identifier(giftId.toHex())
            val event = client?.database()?.query(filter)?.first()

            return event?.content()?.let { UnsignedEvent.fromJson(it) }
        } catch (e: Throwable) {
            throw IllegalStateException("Failed to get cached rumor: ${e.message}", e)
        }
    }

    private suspend fun setCachedRumor(giftId: EventId, rumor: UnsignedEvent) {
        try {
            // Construct reference tags
            val tags = listOf(
                Tag.identifier(giftId.toHex()),
                Tag.publicKey(rumor.author()),
                Tag.custom("r", listOf(rumor.roomId().toString())),
                Tag.custom("k", listOf("14"))
            )

            // Set event kind
            val kind = Kind.fromStd(KindStandard.APPLICATION_SPECIFIC_DATA)

            // Construct event
            val event = EventBuilder(kind, rumor.asJson())
                .tags(tags)
                .finalizeAsync(Keys.generate())

            client?.database()?.saveEvent(event)
        } catch (e: Throwable) {
            println("Failed to set cached rumor: ${e.message}")
        }
    }

    private suspend fun extractRumor(event: Event): UnsignedEvent? {
        try {
            // Gift wrap must have at least one 'p' tag
            if (event.tags().publicKeys().isEmpty()) {
                println("No recipient tags found.")
                return null
            }

            // Event must be a gift wrap
            if (event.kind().asStd().let { it != KindStandard.GIFT_WRAP }) {
                println("Event is not a gift wrap.")
                return null
            }

            // Check if the rumor is already cached
            val cachedRumor = getCachedRumor(event.id())
            if (cachedRumor != null) return cachedRumor

            // Decrypt the gift wrap event
            val seal = signer.nip44DecryptAsync(event.author(), event.content())
            val sealEvent = Event.fromJson(seal)

            // Verify seal event
            if (!sealEvent.verify()) {
                println("Failed to verify seal event.")
                return null
            }

            // Decrypt the rumor
            val rumor = signer.nip44DecryptAsync(sealEvent.author(), sealEvent.content())
            val unsignedEvent = UnsignedEvent.fromJson(rumor)

            // Ensure the rumor author matches the seal
            if (unsignedEvent.author() != sealEvent.author()) {
                println("Author mismatch.")
                return null
            }

            // Cache the rumor for later use
            setCachedRumor(event.id(), unsignedEvent)

            return unsignedEvent
        } catch (e: CancellationException) {
            throw e
        } catch (e: Throwable) {
            println("Failed to unwrap gift ${event.id().toHex()}: ${e.message}")
            return null
        }
    }

    private suspend fun getDefaultRelayList(): Map<RelayUrl, RelayMetadata> {
        // Construct a list of relays
        val relayList = mapOf(
            RelayUrl.parse("wss://relay.damus.io") to RelayMetadata.READ,
            RelayUrl.parse("wss://relay.primal.net") to RelayMetadata.READ,
            RelayUrl.parse("wss://relay.nostr.net") to RelayMetadata.WRITE,
            RelayUrl.parse("wss://nostr.superfriends.online") to RelayMetadata.WRITE
        )

        // Ensure all relays are added and connected
        relayList.forEach { (relay, metadata) ->
            client?.addRelay(
                url = relay,
                capabilities =
                    when (metadata) {
                        RelayMetadata.READ -> RelayCapabilities.read()
                        RelayMetadata.WRITE -> RelayCapabilities.write()
                    }
            )
            client?.connectRelay(relay)
        }

        return relayList
    }

    suspend fun getDefaultMsgRelayList(): List<RelayUrl> {
        // Construct a list of messaging relays
        val msgRelayList = listOf(
            RelayUrl.parse("wss://auth.nostr1.com"),
            RelayUrl.parse("wss://nip17.com"),
        )

        // Ensure all relays are added and connected
        msgRelayList.forEach { relay ->
            client?.addRelay(relay, RelayCapabilities.none())
            client?.connectRelay(relay)
        }

        return msgRelayList
    }

    suspend fun createIdentity(keys: Keys, name: String, bio: String?, picture: String?) {
        // Send relay list event
        val relayList = getDefaultRelayList()
        val relayListEvent = EventBuilder.relayList(relayList).finalizeAsync(keys)

        client?.sendEvent(
            event = relayListEvent,
            target = SendEventTarget.broadcast(),
            ackPolicy = AckPolicy.all(),
            okTimeout = Duration.parse("3s")
        )

        // Send messaging relay list event
        val msgRelayList = getDefaultMsgRelayList()
        val msgRelayListEvent = EventBuilder.nip17RelayList(msgRelayList).finalizeAsync(keys)

        client?.sendEvent(
            event = msgRelayListEvent,
            target = SendEventTarget.toNip65(),
            ackPolicy = AckPolicy.none()
        )

        // Send metadata event
        val metadata =
            Metadata.fromRecord(MetadataRecord(displayName = name, about = bio, picture = picture))
        val metadataEvent = EventBuilder.metadata(metadata).finalizeAsync(keys)

        client?.sendEvent(
            event = metadataEvent,
            target = SendEventTarget.broadcast(),
            ackPolicy = AckPolicy.none()
        )

        // Send contact list event
        val defaultContact =
            Contact(PublicKey.parse("npub1j3rz3ndl902lya6ywxvy5c983lxs8mpukqnx4pa4lt5wrykwl5ys7wpw3x"))
        val contactListEvent = EventBuilder.contactList(listOf(defaultContact)).finalizeAsync(keys)

        client?.sendEvent(
            event = contactListEvent,
            target = SendEventTarget.toNip65(),
            ackPolicy = AckPolicy.none()
        )

        setSigner(keys)
    }

    suspend fun updateProfile(
        name: String? = null,
        bio: String? = null,
        picture: String? = null
    ): Metadata {
        val currentUser = signer.currentUser ?: throw IllegalStateException("User not signed in")

        try {
            val record = getLatestMetadata(currentUser)?.asRecord() ?: MetadataRecord()
            val newRecord = record.copy(
                displayName = name ?: record.displayName,
                about = bio ?: record.about,
                picture = picture ?: record.picture
            )
            val newMetadata = Metadata.fromRecord(newRecord)
            val event = EventBuilder.metadata(newMetadata).finalizeAsync(signer)

            client?.sendEvent(
                event = event,
                target = SendEventTarget.broadcast(),
                ackPolicy = AckPolicy.none()
            )

            return newMetadata
        } catch (e: Exception) {
            throw IllegalStateException("Failed to update identity: ${e.message}", e)
        }
    }

    private suspend fun getLatestMetadata(pubkey: PublicKey): Metadata? {
        return try {
            val kind = Kind.fromStd(KindStandard.METADATA)
            val filter = Filter().kind(kind).author(pubkey).limit(1u)
            val event = client?.database()?.query(filter)?.first() ?: return null

            Metadata.fromJson(event.content())
        } catch (e: Exception) {
            println("Failed to get latest metadata: ${e.message}")
            null
        }
    }

    suspend fun getAllCacheMetadata(): Map<PublicKey, Metadata> {
        try {
            val filter = Filter().kind(Kind.fromStd(KindStandard.METADATA)).limit(100u)
            val events = client?.database()?.query(filter)
            val results = mutableMapOf<PublicKey, Metadata>()

            events?.toVec()?.forEach { event ->
                try {
                    val metadata = Metadata.fromJson(event.content())
                    results[event.author()] = metadata
                } catch (e: Exception) {
                    println("Failed to parse metadata: $e")
                }
            }

            return results
        } catch (e: Exception) {
            println("Failed to get all cache metadata: ${e.message}")
            return emptyMap()
        }
    }

    suspend fun fetchMetadataBatch(keys: List<PublicKey>) {
        try {
            val limit = keys.size.toULong() * 4u
            val opts = SubscribeAutoCloseOptions().exitPolicy(ReqExitPolicy.ExitOnEose)

            // Construct a filter for metadata events
            val filter = Filter()
                .kind(Kind.fromStd(KindStandard.METADATA))
                .authors(keys)
                .limit(limit)

            // Construct a target that includes all filters
            val target =
                ReqTarget.manual(
                    mapOf(
                        RelayUrl.parse("wss://purplepag.es") to listOf(filter),
                        RelayUrl.parse("wss://relay.primal.net") to listOf(filter),
                    )
                )

            client?.subscribe(target = target, closeOn = opts)
        } catch (e: Exception) {
            throw IllegalStateException("Failed to fetch metadata batch: ${e.message}", e)
        }
    }

    suspend fun setMsgRelays(urls: List<RelayUrl>) {
        try {
            val event = EventBuilder.nip17RelayList(urls).finalizeAsync(signer)

            client?.sendEvent(
                event = event,
                target = SendEventTarget.toNip65(),
                ackPolicy = AckPolicy.none(),
            )

            val kind = Kind.fromStd(KindStandard.INBOX_RELAYS)
            val filter = Filter().kind(kind).author(signer.currentUser!!).limit(1u)
            val target = ReqTarget.auto(listOf(filter))
            val opts = SubscribeAutoCloseOptions().exitPolicy(ReqExitPolicy.ExitOnEose)

            client?.subscribe(target = target, closeOn = opts)
        } catch (e: Exception) {
            throw IllegalStateException("Failed to set msg relays: ${e.message}", e)
        }
    }

    suspend fun getMsgRelays(publicKey: PublicKey): List<RelayUrl> {
        try {
            val kind = Kind.fromStd(KindStandard.INBOX_RELAYS)
            val filter = Filter().kind(kind).author(publicKey).limit(1u)
            val events = client?.database()?.query(filter)

            return nip17ExtractRelayList(events?.toVec()?.firstOrNull() ?: return emptyList())
        } catch (e: Exception) {
            throw IllegalStateException("Failed to get msg relays: ${e.message}", e)
        }
    }

    suspend fun fetchMsgRelays(publicKey: PublicKey): List<RelayUrl> {
        try {
            val kind = Kind.fromStd(KindStandard.INBOX_RELAYS)
            val filter = Filter().kind(kind).author(publicKey).limit(1u)
            val target = ReqTarget.auto(listOf(filter))
            val events = client?.fetchEvents(target, timeout = Duration.parse("3s"))

            return nip17ExtractRelayList(events?.toVec()?.firstOrNull() ?: return emptyList())
        } catch (e: Exception) {
            throw IllegalStateException("Failed to fetch msg relays: ${e.message}", e)
        }
    }

    suspend fun getRelayList(publicKey: PublicKey): Map<RelayUrl, RelayMetadata?> {
        try {
            val kind = Kind.fromStd(KindStandard.RELAY_LIST)
            val filter = Filter().kind(kind).author(publicKey).limit(1u)
            val events = client?.database()?.query(filter)

            return extractRelayList(events?.toVec()?.firstOrNull() ?: return emptyMap())
        } catch (e: Exception) {
            throw IllegalStateException("Failed to get relay list: ${e.message}", e)
        }
    }

    suspend fun setRelaylist(relays: Map<RelayUrl, RelayMetadata?>) {
        try {
            val event = EventBuilder.relayList(relays).finalizeAsync(signer)

            client?.sendEvent(
                event = event,
                target = SendEventTarget.broadcast(),
                ackPolicy = AckPolicy.none(),
            )
        } catch (e: Exception) {
            throw IllegalStateException("Failed to set msg relays: ${e.message}", e)
        }
    }

    suspend fun setContactList(contacts: List<PublicKey>) {
        try {
            val contacts = contacts.map { Contact(it) }
            val event = EventBuilder.contactList(contacts).finalizeAsync(signer)

            client?.sendEvent(
                event = event,
                target = SendEventTarget.broadcast(),
                ackPolicy = AckPolicy.none(),
            )
        } catch (e: Exception) {
            throw IllegalStateException("Failed to set contact list: ${e.message}", e)
        }
    }

    suspend fun getChatRooms(): Set<Room>? {
        try {
            val userPubkey =
                signer.getPublicKeyAsync() ?: throw IllegalStateException("User not signed in")

            val kind = Kind.fromStd(KindStandard.APPLICATION_SPECIFIC_DATA)
            val kTag = SingleLetterTag.lowercase(Alphabet.K)

            // Get all DM events
            val filter = Filter().kind(kind).customTags(kTag, listOf("14", "dm"))
            val events = client?.database()?.query(filter)

            // Collect rooms
            val roomsMap: MutableMap<Long, Room> = mutableMapOf()

            events
                ?.toVec()
                ?.map { UnsignedEvent.fromJson(it.content()) }
                ?.filter { it.tags().publicKeys().isNotEmpty() }
                ?.forEach { event ->
                    val newRoom = Room.new(rumor = event, userPubkey = userPubkey)
                    val existingRoom = roomsMap[newRoom.id]

                    // Check if the room already exists
                    if (existingRoom == null || newRoom.createdAt.asSecs() > existingRoom.createdAt.asSecs()) {
                        val rTag = SingleLetterTag.lowercase(Alphabet.R)
                        val filter = Filter().kind(kind).pubkey(userPubkey)
                            .customTag(rTag, newRoom.id.toString())

                        // Determine if it's an ongoing room
                        val isOngoing =
                            client?.database()?.query(filter)?.toVec()?.isNotEmpty() ?: false

                        // Append room to map
                        roomsMap[newRoom.id] =
                            if (isOngoing) newRoom.copy(kind = RoomKind.Ongoing) else newRoom
                    }
                }

            return roomsMap.values.sortedByDescending { it.createdAt.asSecs() }.toSet()
        } catch (e: Exception) {
            println("Failed to get chat rooms: ${e.message}")
            return null
        }
    }

    suspend fun getChatRoomMessages(roomId: Long): List<UnsignedEvent> {
        try {
            val kind = Kind.fromStd(KindStandard.APPLICATION_SPECIFIC_DATA)
            val filter = Filter().kind(kind).reference(roomId.toString())
            val events = client?.database()?.query(filter)

            // Merge the events
            return events
                ?.toVec()
                ?.map { UnsignedEvent.fromJson(it.content()) }
                // Filter out events without public keys (receivers)
                ?.filter { it.tags().publicKeys().isNotEmpty() }
                ?.sortedByDescending { it.createdAt().asSecs() } ?: emptyList()
        } catch (e: Exception) {
            throw IllegalStateException("Failed to get chat room messages: ${e.message}", e)
        }
    }

    suspend fun chatRoomConnect(members: List<PublicKey>) {
        try {
            members.forEach { member ->
                val kind = Kind.fromStd(KindStandard.INBOX_RELAYS)
                val filter = Filter().kind(kind).author(member).limit(1u)

                val stream = client?.streamEvents(
                    target = ReqTarget.auto(listOf(filter)),
                    id = null,
                    timeout = Duration.parse("3s"),
                    policy = ReqExitPolicy.ExitOnEose
                )

                stream?.next()?.let { res ->
                    if (res.event != null) {
                        connectMsgRelays(res.event!!)
                    }
                }
            }
        } catch (e: Exception) {
            throw IllegalStateException("Failed to fetch relays: ${e.message}", e)
        }
    }

    suspend fun connectMsgRelays(event: Event) {
        try {
            val urls = nip17ExtractRelayList(event)
            for (url in urls) {
                client?.addRelay(url, RelayCapabilities.gossip())
                client?.connectRelay(url)
            }
        } catch (e: Exception) {
            throw IllegalStateException("Failed to connect to relays: ${e.message}", e)
        }
    }

    suspend fun sendMessage(
        to: Set<PublicKey>,
        content: String,
        subject: String? = null,
        replies: List<EventId> = emptyList(),
        onRumorCreated: ((UnsignedEvent) -> Unit)? = null,
    ) {
        try {
            val currentUser =
                signer.getPublicKeyAsync() ?: throw IllegalStateException("User not signed in")

            val tags = mutableListOf<Tag>()

            // Add a subject tag if provided
            if (subject != null) {
                tags.add(Tag.custom("subject", listOf(subject)))
            }

            // Add event tags for replies
            if (replies.isNotEmpty()) {
                replies.forEach { replyId ->
                    tags.add(Tag.event(replyId))
                }
            }

            // Add public key tags for each recipient
            to.forEach { pubkey ->
                tags.add(Tag.publicKey(pubkey))
            }

            for (receiver in setOf(currentUser) + to) {
                // Construct the rumor event
                // NEVER SIGN this event with the current user signer
                val rumor = EventBuilder(Kind.fromStd(KindStandard.PRIVATE_DIRECT_MESSAGE), content)
                    .tags(tags)
                    .finalizeUnsigned(currentUser)
                    .ensureId()

                // Emit the rumor to the chat screen
                if (receiver == currentUser) {
                    onRumorCreated?.invoke(rumor)
                }

                // Construct the gift wrap event
                val gift = nip59MakeGiftWrapAsync(
                    signer = signer,
                    receiverPubkey = receiver,
                    rumor = rumor,
                    extraTags = listOf(
                        Tag.custom("k", listOf("14"))
                    )
                )

                // Send the event to receiver's NIP-17 relays
                val output = client?.sendEvent(
                    event = gift,
                    target = SendEventTarget.toNip17(),
                    ackPolicy = AckPolicy.none(),
                    authenticationTimeout = Duration.parse("2s")
                )

                if (output != null) {
                    // Keep track of sent events
                    sentEvents[output.id] = emptyList()
                    if (rumor.id() != null) rumorMap[rumor.id()!!] = output.id

                    // Collect failed outputs
                    output.failed.forEach { (relayUrl, reason) ->
                        println("Failed to send event to relay $relayUrl: $reason")
                    }
                }
            }
        } catch (e: Exception) {
            throw IllegalStateException("Failed to send message: ${e.message}", e)
        }
    }

    suspend fun profileFromAddress(client: HttpClient, address: Nip05Address): Nip05Profile {
        try {
            val response: HttpResponse = client.get(address.url())
            val bodyString: String = response.body()

            return Nip05Profile.fromJson(address, bodyString)
        } catch (e: Exception) {
            throw IllegalStateException("Failed to fetch profile from address: ${e.message}", e)
        }
    }

    suspend fun searchByAddress(query: String): PublicKey {
        try {
            val address = Nip05Address.parse(query)
            val profile = profileFromAddress(HttpClient(), address)

            return profile.publicKey()
        } catch (e: Exception) {
            throw IllegalStateException("Failed to search address: ${e.message}", e)
        }
    }

    suspend fun searchByNostr(query: String): List<PublicKey> {
        try {
            // Add search relay
            val searchRelay = RelayUrl.parse("wss://antiprimal.net")
            if (client?.relay(searchRelay) == null) {
                client?.addRelay(url = searchRelay, capabilities = RelayCapabilities.read())
                client?.connectRelay(searchRelay)
            }

            val kinds = listOf(Kind.fromStd(KindStandard.METADATA))
            val filter = Filter().kinds(kinds).search(query).limit(10u)
            val target = ReqTarget.manual(mapOf(searchRelay to listOf(filter)))

            val stream = client?.streamEvents(
                target = target,
                id = "search",
                timeout = Duration.parse("3s"),
                policy = ReqExitPolicy.ExitOnEose
            )

            // Collect the results
            val results = mutableListOf<PublicKey>()

            // Keep searching until the stream is closed or timeout
            stream?.next()?.let { event ->
                if (event.event != null) {
                    results.add(event.event!!.author())
                }
            }

            return results
        } catch (e: Exception) {
            throw IllegalStateException("Failed to search nostr: ${e.message}", e)
        }
    }

    suspend fun verifyActivity(pubkey: PublicKey): Timestamp? {
        try {
            val filter = Filter().author(pubkey).limit(3u)
            val target = mutableMapOf<RelayUrl, List<Filter>>()
            NostrManager.BOOTSTRAP_RELAYS.forEach { relay ->
                target[RelayUrl.parse(relay)] = listOf(filter)
            }

            val events = client?.fetchEvents(
                target = ReqTarget.manual(target),
                timeout = Duration.parse("3s")
            )

            return events?.first()?.createdAt()
        } catch (e: Exception) {
            throw IllegalStateException("Failed to get latest activity: ${e.message}", e)
        }
    }

    suspend fun verifyContact(pubkey: PublicKey): Boolean {
        try {
            val currentUser =
                signer.getPublicKeyAsync() ?: throw IllegalStateException("User not signed in")

            val kind = Kind.fromStd(KindStandard.CONTACT_LIST)
            val filter = Filter().kind(kind).author(currentUser).limit(1u)

            val events = client?.database()?.query(filter)
            val pubkeys = events?.first()?.tags()?.publicKeys() ?: listOf()

            return pubkeys.contains(pubkey)
        } catch (e: Exception) {
            throw IllegalStateException("Failed to get mutual contacts: ${e.message}", e)
        }
    }

    suspend fun mutualContacts(pubkey: PublicKey): Set<PublicKey> {
        try {
            val currentUser =
                signer.getPublicKeyAsync() ?: throw IllegalStateException("User not signed in")

            val kind = Kind.fromStd(KindStandard.CONTACT_LIST)
            val filter = Filter().kind(kind).pubkey(pubkey).limit(1u)

            val events = client?.database()?.query(filter)
            val contacts = mutableSetOf<PublicKey>()

            events?.toVec()?.filter { it.author() != currentUser }?.forEach { event ->
                contacts.add(event.author())
            }

            return contacts.toSet()
        } catch (e: Exception) {
            throw IllegalStateException("Failed to get mutual contacts: ${e.message}", e)
        }
    }
}

data class MessageSyncState(
    val processedCount: Int = 0,
    val isSyncing: Boolean = false
)
