package su.reya.coop.nostr

import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.supervisorScope
import kotlinx.coroutines.withTimeoutOrNull
import rust.nostr.sdk.AsyncNostrSigner
import rust.nostr.sdk.Client
import rust.nostr.sdk.ClientBuilder
import rust.nostr.sdk.ClientNotification
import rust.nostr.sdk.Event
import rust.nostr.sdk.EventId
import rust.nostr.sdk.GossipConfig
import rust.nostr.sdk.Keys
import rust.nostr.sdk.KindStandard
import rust.nostr.sdk.LogLevel
import rust.nostr.sdk.Metadata
import rust.nostr.sdk.NostrDatabase
import rust.nostr.sdk.NostrGossip
import rust.nostr.sdk.PublicKey
import rust.nostr.sdk.RelayMessageEnum
import rust.nostr.sdk.SignerAuthenticator
import rust.nostr.sdk.SleepWhenIdle
import rust.nostr.sdk.Timestamp
import rust.nostr.sdk.UnsignedEvent
import rust.nostr.sdk.initLogger
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds

object NostrManager {
    val instance = Nostr()
}

class Nostr(
    private val defaultDispatcher: CoroutineDispatcher = Dispatchers.Default,
) {
    var client: Client? = null
        private set
    var signer: UniversalSigner = UniversalSigner(Keys.generate())
        private set

    val messages = MessageManager(this)
    val profiles = ProfileManager(this)
    val relays = RelayManager(this)

    private val isInitialized = MutableStateFlow(false)

    private val _newEvents = MutableSharedFlow<UnsignedEvent>(
        replay = 0,
        extraBufferCapacity = 100,
        onBufferOverflow = BufferOverflow.SUSPEND
    )
    val newEvents = _newEvents.asSharedFlow()

    fun emitNewEvent(event: UnsignedEvent) {
        _newEvents.tryEmit(event)
    }

    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()

            // Initialize the authenticator
            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() {
        withTimeoutOrNull(30.seconds) { isInitialized.first { it } }
            ?: throw IllegalStateException("Nostr initialization timed out")
    }

    suspend fun connectBootstrapRelays() {
        relays.connectBootstrapRelays()
    }

    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.publicKeyFlow.value == event.author()
        } catch (e: Exception) {
            println("Failed to check if event is signed by user: ${e.message}")
            false
        }
    }

    @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>(1024)
        var processedCount = 0
        var eoseReceived = false

        launch(defaultDispatcher) {
            for (event in giftWrapQueue) {
                val rumor = messages.extractRumor(event)
                processedCount++

                // Trigger new message notification
                if (rumor != null) {
                    val isSelfMessage = rumor.author() == signer.publicKeyFlow.value
                    val isNew = rumor.createdAt().asSecs() >= now.asSecs()

                    if (isNew && !isSelfMessage) {
                        onNewMessage(rumor)
                    }
                }

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

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

            when (notification) {
                is ClientNotification.Message -> {
                    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
                                        profiles.syncMutualContacts(pubkeys)
                                        // Emit contact list update
                                        onContactListUpdate(pubkeys)
                                    }
                                }

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

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

                                else -> {}
                            }
                        }

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

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

                is ClientNotification.Shutdown -> {
                    break
                }

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