package com.bitchat.android.ui

import android.app.Application
import android.util.Log
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner
import androidx.lifecycle.viewModelScope
import com.bitchat.android.nostr.GeohashMessageHandler
import com.bitchat.android.nostr.GeohashRepository
import com.bitchat.android.nostr.NostrDirectMessageHandler
import com.bitchat.android.nostr.NostrIdentityBridge
import com.bitchat.android.nostr.NostrProtocol
import com.bitchat.android.nostr.NostrRelayManager
import com.bitchat.android.nostr.NostrSubscriptionManager
import com.bitchat.android.nostr.PoWPreferenceManager
import com.bitchat.android.nostr.GeohashAliasRegistry
import com.bitchat.android.nostr.GeohashConversationRegistry
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import java.util.Date
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.isActive
import kotlinx.coroutines.Dispatchers
import java.security.SecureRandom
import kotlin.random.asKotlinRandom

class GeohashViewModel(
    application: Application,
    private val state: ChatState,
    private val messageManager: MessageManager,
    private val privateChatManager: PrivateChatManager,
    private val meshDelegateHandler: MeshDelegateHandler,
    private val dataManager: DataManager,
    private val notificationManager: NotificationManager
) : AndroidViewModel(application), DefaultLifecycleObserver {

    companion object { 
        private const val TAG = "GeohashViewModel" 
        private val secureRandom = SecureRandom().asKotlinRandom()
    }

    private val repo = GeohashRepository(application, state, dataManager)
    private val subscriptionManager = NostrSubscriptionManager(application, viewModelScope)
    private val geohashMessageHandler = GeohashMessageHandler(
        application = application,
        state = state,
        messageManager = messageManager,
        repo = repo,
        scope = viewModelScope,
        dataManager = dataManager
    )
    private val dmHandler = NostrDirectMessageHandler(
        application = application,
        state = state,
        privateChatManager = privateChatManager,
        meshDelegateHandler = meshDelegateHandler,
        scope = viewModelScope,
        repo = repo,
        dataManager = dataManager
    )

    // Live channel message stream (kind 20000). Low-volume; kept alive in the background.
    private var currentGeohashMsgSubId: String? = null
    // Presence heartbeat firehose (kind 20001). High-volume; paused while backgrounded.
    private var currentGeohashPresenceSubId: String? = null
    private var currentDmSubId: String? = null
    private var geoTimer: Job? = null
    private var globalPresenceJob: Job? = null
    private var locationChannelManager: com.bitchat.android.geohash.LocationChannelManager? = null
    private val activeSamplingGeohashes = mutableSetOf<String>()

    // Geohash of the currently selected Location channel (null for Mesh/none).
    private var activeChannelGeohash: String? = null

    val geohashPeople: StateFlow<List<GeoPerson>> = state.geohashPeople
    val geohashParticipantCounts: StateFlow<Map<String, Int>> = state.geohashParticipantCounts
    val selectedLocationChannel: StateFlow<com.bitchat.android.geohash.ChannelID?> = state.selectedLocationChannel

    fun initialize() {
        subscriptionManager.connect()
        // Observe process lifecycle to manage background sampling
        kotlin.runCatching {
            ProcessLifecycleOwner.get().lifecycle.addObserver(this)
        }
        val identity = NostrIdentityBridge.getCurrentNostrIdentity(getApplication())
        if (identity != null) {
            // Use global chat-messages only for full account DMs (mesh context). For geohash DMs, subscribe per-geohash below.
            subscriptionManager.subscribeGiftWraps(
                pubkey = identity.publicKeyHex,
                sinceMs = System.currentTimeMillis() - 172800000L,
                id = "chat-messages",
                handler = { event -> dmHandler.onGiftWrap(event, "", identity) } // geohash="" means global account DM (not geohash identity)
            )
        }
        try {
            locationChannelManager = com.bitchat.android.geohash.LocationChannelManager.getInstance(getApplication())
            viewModelScope.launch {
                locationChannelManager?.selectedChannel?.collect { channel ->
                    state.setSelectedLocationChannel(channel)
                    switchLocationChannel(channel)
                }
            }
            viewModelScope.launch {
                locationChannelManager?.teleported?.collect { teleported ->
                    state.setIsTeleported(teleported)
                }
            }
            
            // Start global presence heartbeat loop
            startGlobalPresenceHeartbeat()
            
        } catch (e: Exception) {
            Log.e(TAG, "Failed to initialize location channel state: ${e.message}")
            state.setSelectedLocationChannel(com.bitchat.android.geohash.ChannelID.Mesh)
            state.setIsTeleported(false)
        }
    }

    private fun startGlobalPresenceHeartbeat() {
        globalPresenceJob?.cancel()
        globalPresenceJob = viewModelScope.launch(kotlinx.coroutines.Dispatchers.IO) {
            // Reactively restart heartbeat whenever available channels change
            locationChannelManager?.availableChannels?.collectLatest { channels ->
                // Filter for REGION (2), PROVINCE (4), CITY (5) - precision <= 5
                val targetGeohashes = channels.filter { it.level.precision <= 5 }.map { it.geohash }

                if (targetGeohashes.isNotEmpty()) {
                    // Enter heartbeat loop for this set of channels
                    // If channels change (e.g. user moves), collectLatest cancels this loop and starts a new one immediately
                    while (true) {
                        // Randomize loop interval (40-80s, average 60s)
                        val loopInterval = secureRandom.nextLong(40000L, 80000L)
                        var timeSpent = 0L

                        try {
                            Log.v(TAG, "💓 Broadcasting global presence to ${targetGeohashes.size} channels")
                            targetGeohashes.forEach { geohash ->
                                // Decorrelate individual broadcasts with random delay (1s-5s)
                                val stepDelay = secureRandom.nextLong(1000L, 10000L)
                                delay(stepDelay)
                                timeSpent += stepDelay
                                
                                broadcastPresence(geohash)
                            }
                        } catch (e: Exception) {
                            Log.w(TAG, "Global presence heartbeat error: ${e.message}")
                        }
                        
                        // Wait remaining time to satisfy target average cadence
                        val remaining = loopInterval - timeSpent
                        if (remaining > 0) {
                            delay(remaining)
                        } else {
                            delay(10000L) // Minimum guard delay
                        }
                    }
                }
            }
        }
    }

    fun panicReset() {
        repo.clearAll()
        GeohashAliasRegistry.clear()
        GeohashConversationRegistry.clear()
        subscriptionManager.disconnect()
        currentGeohashMsgSubId = null
        currentGeohashPresenceSubId = null
        currentDmSubId = null
        activeChannelGeohash = null
        geoTimer?.cancel()
        geoTimer = null
        globalPresenceJob?.cancel()
        globalPresenceJob = null
        try { NostrIdentityBridge.clearAllAssociations(getApplication()) } catch (_: Exception) {}
        initialize()
    }

    private suspend fun broadcastPresence(geohash: String) {
        try {
            val identity = NostrIdentityBridge.deriveIdentity(geohash, getApplication())
            val event = NostrProtocol.createGeohashPresenceEvent(geohash, identity)
            val relayManager = NostrRelayManager.getInstance(getApplication())
            // Presence is lightweight, send to geohash relays
            relayManager.sendEventToGeohash(event, geohash, includeDefaults = false, nRelays = 5)
            Log.v(TAG, "💓 Sent presence heartbeat for $geohash")
        } catch (e: Exception) {
            Log.w(TAG, "Failed to send presence for $geohash: ${e.message}")
        }
    }

    fun sendGeohashMessage(content: String, channel: com.bitchat.android.geohash.GeohashChannel, myPeerID: String, nickname: String?) {
        viewModelScope.launch {
            try {
                val tempId = "temp_${System.currentTimeMillis()}_${kotlin.random.Random.nextInt(1000)}"
                val pow = PoWPreferenceManager.getCurrentSettings()
                val localMsg = com.bitchat.android.model.BitchatMessage(
                    id = tempId,
                    sender = nickname ?: myPeerID,
                    content = content,
                    timestamp = Date(),
                    isRelay = false,
                    senderPeerID = "geohash:${channel.geohash}",
                    channel = "#${channel.geohash}",
                    powDifficulty = if (pow.enabled) pow.difficulty else null
                )
                messageManager.addChannelMessage("geo:${channel.geohash}", localMsg)
                val startedMining = pow.enabled && pow.difficulty > 0
                if (startedMining) {
                    com.bitchat.android.ui.PoWMiningTracker.startMiningMessage(tempId)
                }
                try {
                    val identity = NostrIdentityBridge.deriveIdentity(forGeohash = channel.geohash, context = getApplication())
                    val teleported = state.isTeleported.value
                    val event = NostrProtocol.createEphemeralGeohashEvent(content, channel.geohash, identity, nickname, teleported)
                    val relayManager = NostrRelayManager.getInstance(getApplication())
                    relayManager.sendEventToGeohash(event, channel.geohash, includeDefaults = false, nRelays = 5)
                } finally {
                    // Ensure we stop the per-message mining animation regardless of success/failure
                    if (startedMining) {
                        com.bitchat.android.ui.PoWMiningTracker.stopMiningMessage(tempId)
                    }
                }
            } catch (e: Exception) {
                Log.e(TAG, "Failed to send geohash message: ${e.message}")
            }
        }
    }

    fun beginGeohashSampling(geohashes: List<String>) {
        if (geohashes.isEmpty()) {
            endGeohashSampling()
            return
        }
        
        // Diffing logic to avoid redundant REQ and leaks
        val currentSet = activeSamplingGeohashes.toSet()
        val newSet = geohashes.toSet()

        val toRemove = currentSet - newSet
        val toAdd = newSet - currentSet

        if (toAdd.isEmpty() && toRemove.isEmpty()) return

        Log.d(TAG, "🌍 Updating sampling: +${toAdd.size} new, -${toRemove.size} removed")
        
        // Remove old subscriptions
        toRemove.forEach { geohash ->
            subscriptionManager.unsubscribe("sampling-$geohash")
            activeSamplingGeohashes.remove(geohash)
        }

        // Add new subscriptions
        activeSamplingGeohashes.addAll(toAdd)
        if (isAppInForeground()) {
            toAdd.forEach { geohash ->
                performSubscribeSampling(geohash)
            }
        }
    }

    fun endGeohashSampling() { 
        if (activeSamplingGeohashes.isEmpty()) return
        Log.d(TAG, "🌍 Ending geohash sampling (cleaning up ${activeSamplingGeohashes.size} subs)")
        
        activeSamplingGeohashes.toList().forEach { geohash ->
            subscriptionManager.unsubscribe("sampling-$geohash")
        }
        activeSamplingGeohashes.clear()
    }
    fun geohashParticipantCount(geohash: String): Int = repo.geohashParticipantCount(geohash)
    fun isPersonTeleported(pubkeyHex: String): Boolean = repo.isPersonTeleported(pubkeyHex)

    fun startGeohashDM(pubkeyHex: String, onStartPrivateChat: (String) -> Unit) {
        val convKey = "nostr_${pubkeyHex.take(16)}"
        repo.putNostrKeyMapping(convKey, pubkeyHex)
        // Record the conversation's geohash using the currently selected location channel (if any)
        val current = state.selectedLocationChannel.value
        val gh = (current as? com.bitchat.android.geohash.ChannelID.Location)?.channel?.geohash
        if (!gh.isNullOrEmpty()) {
            repo.setConversationGeohash(convKey, gh)
            GeohashConversationRegistry.set(convKey, gh)
        }
        onStartPrivateChat(convKey)
        Log.d(TAG, "🗨️ Started geohash DM with ${pubkeyHex} -> ${convKey} (geohash=${gh})")
    }

    fun startGeohashDMByNickname(nickname: String, onStartPrivateChat: (String) -> Unit) {
        val pubkey = repo.findPubkeyByNickname(nickname)
        if (pubkey != null) {
            startGeohashDM(pubkey, onStartPrivateChat)
        } else {
            Log.w(TAG, "Cannot start geohash DM: nickname '$nickname' not found in repo")
            // Optionally notify user
        }
    }

    fun startGeohashDMByShortId(shortId: String, onStartPrivateChat: (String) -> Unit) {
        val pubkey = repo.findPubkeyByShortId(shortId)
        if (pubkey != null) {
            startGeohashDM(pubkey, onStartPrivateChat)
        } else {
             Log.w(TAG, "Cannot start geohash DM: shortId '$shortId' not found in repo")
        }
    }

    fun getNostrKeyMapping(): Map<String, String> = repo.getNostrKeyMapping()

    fun blockUserInGeohash(targetNickname: String) {
        val pubkey = repo.findPubkeyByNickname(targetNickname)
        if (pubkey != null) {
            dataManager.addGeohashBlockedUser(pubkey)
            // Refresh people list and counts to remove blocked entry immediately
            repo.refreshGeohashPeople()
            repo.updateReactiveParticipantCounts()
            val sysMsg = com.bitchat.android.model.BitchatMessage(
                sender = "system",
                content = "blocked $targetNickname in geohash channels",
                timestamp = Date(),
                isRelay = false
            )
            messageManager.addMessage(sysMsg)
        } else {
            val sysMsg = com.bitchat.android.model.BitchatMessage(
                sender = "system",
                content = "user '$targetNickname' not found in current geohash",
                timestamp = Date(),
                isRelay = false
            )
            messageManager.addMessage(sysMsg)
        }
    }

    fun selectLocationChannel(channel: com.bitchat.android.geohash.ChannelID) {
        locationChannelManager?.select(channel) ?: run { Log.w(TAG, "Cannot select location channel - not initialized") }
    }

    fun displayNameForNostrPubkeyUI(pubkeyHex: String): String = repo.displayNameForNostrPubkeyUI(pubkeyHex)
    fun displayNameForGeohashConversation(pubkeyHex: String, sourceGeohash: String): String = repo.displayNameForGeohashConversation(pubkeyHex, sourceGeohash)

    fun colorForNostrPubkey(pubkeyHex: String, isDark: Boolean): androidx.compose.ui.graphics.Color {
        val seed = "nostr:${pubkeyHex.lowercase()}"
        return colorForPeerSeed(seed, isDark).copy()
    }

    private fun switchLocationChannel(channel: com.bitchat.android.geohash.ChannelID?) {
        geoTimer?.cancel(); geoTimer = null
        currentGeohashMsgSubId?.let { subscriptionManager.unsubscribe(it); currentGeohashMsgSubId = null }
        currentGeohashPresenceSubId?.let { subscriptionManager.unsubscribe(it); currentGeohashPresenceSubId = null }
        currentDmSubId?.let { subscriptionManager.unsubscribe(it); currentDmSubId = null }

        when (channel) {
            is com.bitchat.android.geohash.ChannelID.Mesh -> {
                Log.d(TAG, "📡 Switched to mesh channel")
                activeChannelGeohash = null
                repo.setCurrentGeohash(null)
                notificationManager.setCurrentGeohash(null)
                notificationManager.clearMeshMentionNotifications()
                repo.refreshGeohashPeople()
            }
            is com.bitchat.android.geohash.ChannelID.Location -> {
                Log.d(TAG, "📍 Switching to geohash channel: ${channel.channel.geohash}")
                activeChannelGeohash = channel.channel.geohash
                repo.setCurrentGeohash(channel.channel.geohash)
                repo.refreshGeohashPeople()
                notificationManager.setCurrentGeohash(channel.channel.geohash)
                notificationManager.clearNotificationsForGeohash(channel.channel.geohash)
                try { messageManager.clearChannelUnreadCount("geo:${channel.channel.geohash}") } catch (_: Exception) { }

                try {
                    val identity = NostrIdentityBridge.deriveIdentity(channel.channel.geohash, getApplication())
                    // We don't update participant here anymore; presence loop handles it via Kind 20001
                    val teleported = state.isTeleported.value
                    if (teleported) repo.markTeleported(identity.publicKeyHex)
                } catch (e: Exception) { Log.w(TAG, "Failed identity setup: ${e.message}") }

                startGeoParticipantsTimer()

                // Chat message stream (kind 20000) is low-volume; keep it alive even when
                // backgrounded so geohash messages still arrive.
                subscribeChannelMessages(channel.channel.geohash)
                // Presence heartbeat firehose (kind 20001) is the high-volume data hog; only
                // run it in the foreground. It is restored in onStart() and torn down in onStop().
                if (isAppInForeground()) {
                    subscribeChannelPresence(channel.channel.geohash)
                }
                // Gift-wrap DM subscription is lightweight (filtered to our pubkey) and is
                // kept alive in the background so geohash DMs still arrive.
                subscribeChannelDM(channel.channel.geohash)
            }
            null -> {
                Log.d(TAG, "📡 No channel selected")
                repo.setCurrentGeohash(null)
                repo.refreshGeohashPeople()
            }
        }
    }

    /**
     * Subscribe to the chat message stream (kind 20000) for a geohash channel.
     * Low-volume; kept alive in the background so messages keep arriving.
     */
    private fun subscribeChannelMessages(geohash: String) {
        val subId = "geohash-$geohash"; currentGeohashMsgSubId = subId
        subscriptionManager.subscribeGeohashMessages(
            geohash = geohash,
            sinceMs = System.currentTimeMillis() - 3600000L,
            limit = 200,
            id = subId,
            handler = { event -> geohashMessageHandler.onEvent(event, geohash) }
        )
    }

    /**
     * Subscribe to the presence heartbeat firehose (kind 20001) for a geohash channel.
     * High-volume; only used to refresh the participant list, so it is torn down in
     * onStop() and restored in onStart() to cut background mobile data.
     */
    private fun subscribeChannelPresence(geohash: String) {
        val subId = "geohash-presence-$geohash"; currentGeohashPresenceSubId = subId
        subscriptionManager.subscribeGeohashPresence(
            geohash = geohash,
            sinceMs = System.currentTimeMillis() - 3600000L,
            limit = 200,
            id = subId,
            handler = { event -> geohashMessageHandler.onEvent(event, geohash) }
        )
    }

    /**
     * Subscribe to gift-wrap DMs for a geohash channel's derived identity.
     * Lightweight (filtered to our pubkey); kept alive in the background.
     */
    private fun subscribeChannelDM(geohash: String) {
        viewModelScope.launch {
            val dmIdentity = NostrIdentityBridge.deriveIdentity(geohash, getApplication())
            val dmSubId = "geo-dm-$geohash"; currentDmSubId = dmSubId
            subscriptionManager.subscribeGiftWraps(
                pubkey = dmIdentity.publicKeyHex,
                sinceMs = System.currentTimeMillis() - 172800000L,
                id = dmSubId,
                handler = { event -> dmHandler.onGiftWrap(event, geohash, dmIdentity) }
            )
            // Also register alias in global registry for routing convenience
            GeohashAliasRegistry.put("nostr_${dmIdentity.publicKeyHex.take(16)}", dmIdentity.publicKeyHex)
        }
    }

    private fun startGeoParticipantsTimer() {
        geoTimer = viewModelScope.launch {
            while (repo.getCurrentGeohash() != null) {
                delay(30000)
                repo.refreshGeohashPeople()
            }
        }
    }

    override fun onCleared() {
        super.onCleared()
        kotlin.runCatching {
            ProcessLifecycleOwner.get().lifecycle.removeObserver(this)
        }
    }

    override fun onStart(owner: LifecycleOwner) {
        Log.d(TAG, "🌍 App foregrounded: resuming Nostr streaming")
        // Restore the presence heartbeat firehose for the selected geohash channel.
        // (The chat message stream is kept alive in the background, so it is not restored here.)
        activeChannelGeohash?.let { subscribeChannelPresence(it) }
        // Resume geohash sampling subscriptions
        activeSamplingGeohashes.forEach { performSubscribeSampling(it) }
        // Resume the participant-refresh polling timer if a geohash is selected
        if (repo.getCurrentGeohash() != null && geoTimer?.isActive != true) {
            startGeoParticipantsTimer()
        }
        // Resume the global presence heartbeat
        if (globalPresenceJob?.isActive != true) {
            startGlobalPresenceHeartbeat()
        }
    }

    override fun onStop(owner: LifecycleOwner) {
        Log.d(TAG, "🌍 App backgrounded: pausing geohash presence firehose (keeping message + DM subscriptions)")
        // Drop the high-volume presence heartbeat firehose (kind 20001).
        // The chat message stream (kind 20000) is intentionally left active so messages still arrive.
        currentGeohashPresenceSubId?.let { subscriptionManager.unsubscribe(it); currentGeohashPresenceSubId = null }
        // Drop geohash sampling subscriptions
        activeSamplingGeohashes.forEach { subscriptionManager.unsubscribe("sampling-$it") }
        // Stop broadcasting presence heartbeats
        globalPresenceJob?.cancel(); globalPresenceJob = null
        // Stop participant-refresh polling
        geoTimer?.cancel(); geoTimer = null
        // NOTE: gift-wrap DM subscriptions (per-geohash + global "chat-messages") are intentionally
        // left active so direct messages still arrive while backgrounded.
    }

    private fun performSubscribeSampling(geohash: String) {
        // Sampling only needs participant counts, never message bodies, so it subscribes to
        // presence heartbeats only (kind 20001) to keep the payload small.
        subscriptionManager.subscribeGeohashPresence(
            geohash = geohash,
            sinceMs = System.currentTimeMillis() - 86400000L,
            limit = 200,
            id = "sampling-$geohash",
            handler = { event -> geohashMessageHandler.onEvent(event, geohash) }
        )
    }

    private fun isAppInForeground(): Boolean {
        return ProcessLifecycleOwner.get().lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)
    }
}
