Pith - wedge
package com.vgmlr.wedge

import android.content.Context
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.unit.sp
import androidx.core.content.edit
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay

private const val SYNC_FLASH_MS = 3_000L

class MainViewModel(
    val dao: NoteDao,
    val prefs: PreferenceManager,
    context: Context
) : ViewModel() {
    
    private val appContext = context.applicationContext
    private val syncPrefs = appContext.getSharedPreferences("sync_prefs", Context.MODE_PRIVATE)
    private val _syncStatus = MutableStateFlow("Disconnected")
    val syncStatus = _syncStatus.asStateFlow()

    private val _isLinkUp = MutableStateFlow(false)
    val isLinkUp = _isLinkUp.asStateFlow()

    private val _peerLastModified = MutableStateFlow(0L)

    private val _autoMerge = MutableStateFlow(syncPrefs.getBoolean("auto_merge", false))
    val autoMerge = _autoMerge.asStateFlow()
    private var idleJob: Job? = null
    
    // bump editor on sync
    private val _syncEvent = MutableStateFlow(0L)
    val syncEvent = _syncEvent.asStateFlow()

    private val _syncFlash = MutableStateFlow(false)
    val syncFlash = _syncFlash.asStateFlow()
    private var flashJob: Job? = null

    private val _olderPrompt = MutableStateFlow(false)
    val olderPrompt = _olderPrompt.asStateFlow()

    private var currentPassKey = ""

    init {
        viewModelScope.launch {
            prefs.passKey.collect { currentPassKey = it }
        }
    }

    fun updatePassKey(newKey: String) {
        viewModelScope.launch {
            prefs.setPassKey(newKey)
        }
    }

    fun setAutoMerge(on: Boolean) {
        _autoMerge.value = on
        syncPrefs.edit { putBoolean("auto_merge", on) }
        peer.refreshMeta()
        autoMergeTick()
    }

    fun noteActivity() {
        if (!peer.autoMergeReady()) return
        idleJob?.cancel()
        idleJob = viewModelScope.launch {
            // real-time sync delay
            delay(3000)
            autoMergeTick()
        }
    }

    private fun autoMergeTick() {
        if (!peer.autoMergeReady()) return
        viewModelScope.launch(Dispatchers.IO) {
            val note = dao.getNoteSync()
            val local = note?.lastModified ?: 0L
            if (local <= _peerLastModified.value) return@launch 
            doSend(note?.content ?: "", local)
        }
    }

    private val peer = PeerManager(
        getLocalPassKey = { currentPassKey },
        getLocalLastModified = { dao.getNoteSync()?.lastModified ?: 0L },
        getAutoMerge = { _autoMerge.value },
        onPeerMeta = { peerDate -> _peerLastModified.value = peerDate },
        onMessageReceived = { cleanMessage, peerDate ->
            viewModelScope.launch(Dispatchers.IO) {
                if (!archive()) return@launch
                val stamp = if (peerDate > 0L) peerDate else System.currentTimeMillis()
                dao.save(NoteEntity(id = 1, content = cleanMessage, lastModified = stamp))
                NoteWidgetProvider.triggerUpdate(appContext)
                syncPrefs.edit {
                    putLong("sync_last_stamp", System.currentTimeMillis())
                }
                withContext(Dispatchers.Main) {
                    _syncStatus.value = "Received"
                    _syncEvent.value = System.currentTimeMillis()
                    flashSync()
                    noteActivity()
                }
            }
        },
        onLinkReady = { autoMergeTick() },
        onStatusChanged = { status, isConnected ->
            viewModelScope.launch {
                _syncStatus.value = status
                _isLinkUp.value = isConnected
            }
        }
    )

    init {
        peer.startListener()
    }

    fun trySendNoteText(route: String, peerHost: String) {
        viewModelScope.launch(Dispatchers.IO) {
            val host = if (route == "USB") "127.0.0.1" else peerHost
            if (!peer.dial(host, 35691)) return@launch
            val note = dao.getNoteSync()
            val localDate = note?.lastModified ?: 0L
            val peerDate = _peerLastModified.value
            if (peerDate != 0L && localDate < peerDate) {
                _olderPrompt.value = true
            } else {
                doSend(note?.content ?: "", localDate)
            }
        }
    }

    fun dismissOlderPrompt() {
        _olderPrompt.value = false
    }

    fun confirmSendNoteText() {
        if (!peer.isLinkUp()) return
        viewModelScope.launch(Dispatchers.IO) {
            val note = dao.getNoteSync()
            doSend(note?.content ?: "", note?.lastModified ?: 0L)
        }
    }

    // headless send
    fun quickSend() {
        val route = syncPrefs.getString("route", "USB") ?: "USB"
        val host = syncPrefs.getString("peer_ip", "") ?: ""
        if (route != "USB" && route != "WIFI") return
        trySendNoteText(route, host)
    }

    // archive first
    private suspend fun archive(): Boolean {
        if (!_autoMerge.value) return true
        return runCatching { WedgeBackup.run(appContext) }
            .onFailure { _syncStatus.value = "Archive Failed" }
            .isSuccess
    }

    private suspend fun doSend(text: String, date: Long) {
        if (!archive()) return
        peer.sendTextMessage(text, currentPassKey, date, onSuccess = {
            _syncStatus.value = "Sent"
            flashSync()
        })
    }

    private fun flashSync() {
        flashJob?.cancel()
        flashJob = viewModelScope.launch {
            _syncFlash.value = true
            delay(SYNC_FLASH_MS)
            _syncFlash.value = false
        }
    }
    
    val editorStyle = prefs.settings.map { settings ->
        TextStyle(
            fontSize = settings.editorFontSize.sp,
            fontFamily = FontFamily.Monospace,
            lineHeight = (settings.editorFontSize * settings.lineHeight).sp,
            color = parseColor(settings.textColor)
        )
    }.stateIn(
        viewModelScope,
        SharingStarted.Eagerly,
        TextStyle(
            fontSize = WedgeConfig.FONT_SIZE_DEFAULT.sp,
            fontFamily = FontFamily.Monospace,
            lineHeight = (WedgeConfig.FONT_SIZE_DEFAULT * WedgeConfig.LINE_HEIGHT_DEFAULT).sp,
            color = parseColor(WedgeConfig.TEXT_COLOR_DEFAULT)
        )
    )

    override fun onCleared() {
        super.onCleared()
        peer.stop()
    }
}

class MainViewModelFactory(
    private val dao: NoteDao,
    private val prefs: PreferenceManager,
    private val context: Context
) : androidx.lifecycle.ViewModelProvider.Factory {
    @Suppress("UNCHECKED_CAST")
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        return MainViewModel(dao, prefs, context) as T
    }
}