MailboxManager.kt

/*
 * This file is part of GNU Taler
 * (C) 2026 Taler Systems S.A.
 *
 * GNU Taler is free software; you can redistribute it and/or modify it under the
 * terms of the GNU General Public License as published by the Free Software
 * Foundation; either version 3, or (at your option) any later version.
 *
 * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 * A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along with
 * GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
 */

package net.taler.wallet.mailbox

import android.util.Log
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import net.taler.common.Amount
import net.taler.common.Timestamp
import net.taler.wallet.backend.BackendManager
import net.taler.wallet.backend.TalerErrorCode
import net.taler.wallet.backend.TalerErrorInfo
import net.taler.wallet.backend.WalletBackendApi
import net.taler.wallet.main.TAG
import net.taler.wallet.peer.PreparePeerPullDebitResponse
import net.taler.wallet.peer.PreparePeerPushCreditResponse
import org.json.JSONObject

@Serializable
data class MailboxConfiguration(
    val mailboxBaseUrl: String,
    val privateKey: String,
    val privateEncryptionKey: String,
    val hAddress: String,
    val expiration: Timestamp,
    val payUri: String? = null,
) {
    override fun toString(): String {
        return "MailboxConfiguration(mailboxBaseUrl='$mailboxBaseUrl', privateKey='REDACTED', privateEncryptionKey='REDACTED', hAddress='$hAddress', expiration=$expiration, payUri=$payUri)"
    }
}

@Serializable
data class MailboxMessageRecord(
    val originMailboxBaseUrl: String,
    val downloadedAt: Timestamp,
    val talerUri: String,
    val subjectHint: String? = null,
)

@Serializable
data class MailboxMessagesResponse(
    val messages: List<MailboxMessageRecord>,
)

@Serializable
data class MailboxRefreshResponse(
    val messages: List<MailboxMessageRecord>,
    val mailboxExpiration: Timestamp? = null,
)

@Serializable
data class MailboxResponse(
    val mailboxConfiguration: MailboxConfiguration? = null,
)

@Serializable
data class MailboxMessageDetails(
    val summary: String? = null,
    val amount: Amount? = null,
)

sealed class MailboxResult {
    data object Loading : MailboxResult()
    data class Error(val error: TalerErrorInfo) : MailboxResult()
    data class Success(
        val config: MailboxConfiguration,
        val messages: List<MailboxMessageRecord>,
    ) : MailboxResult()
}

class MailboxManager(
    private val api: WalletBackendApi,
    private val scope: CoroutineScope,
    private val isDevMode: () -> Boolean,
    private val onMessage: ((msg: String, actionLabel: String?, onAction: (() -> Unit)?) -> Unit)? = null,
) {
    private val _mailbox = MutableStateFlow<MailboxResult>(MailboxResult.Loading)
    val mailbox = _mailbox.asStateFlow()

    private val _messageDetails = MutableStateFlow<Map<String, MailboxMessageDetails>>(emptyMap())
    val messageDetails = _messageDetails.asStateFlow()

    private var started = false

    fun start() {
        if (started) return
        started = true
        loadMailbox()
        scope.launch {
            mailbox.first { it is MailboxResult.Success }
            while (true) {
                delay(30000)
                if (isDevMode()) {
                    val currentState = _mailbox.value
                    if (currentState is MailboxResult.Success) {
                        refreshMailbox(currentState.config)
                    }
                }
            }
        }
    }

    fun prepareAndStoreSubject(message: MailboxMessageRecord, onReady: () -> Unit) {
        val uri = message.talerUri
        if (!message.subjectHint.isNullOrBlank()) {
            scope.launch(Dispatchers.Main) { onReady() }
            return
        }

        scope.launch(Dispatchers.IO) {
            try {
                var summary: String? = null
                if (uri.startsWith("taler://pay-push")) {
                    api.request("preparePeerPushCredit", PreparePeerPushCreditResponse.serializer()) {
                        put("talerUri", uri)
                    }.onSuccess { res ->
                        summary = res.contractTerms.summary.ifBlank { "Incoming payment" }
                    }.onError {
                        summary = "Incoming payment"
                    }
                } else if (uri.startsWith("taler://pay-pull")) {
                    api.request("preparePeerPullDebit", PreparePeerPullDebitResponse.serializer()) {
                        put("talerUri", uri)
                    }.onSuccess { res ->
                        summary = res.contractTerms.summary.ifBlank { "Payment request" }
                    }.onError {
                        summary = "Payment request"
                    }
                } else {
                    summary = "Transaction"
                }

                if (!summary.isNullOrBlank()) {
                    _messageDetails.update {
                        it + (uri to MailboxMessageDetails(
                            summary = summary,
                            amount = null
                        ))
                    }
                    val updated = message.copy(subjectHint = summary)
                    api.request<Unit>("upsertMailboxMessage") {
                        put("message", JSONObject(BackendManager.json.encodeToString(updated)))
                    }.onSuccess {
                        val state = _mailbox.value
                        if (state is MailboxResult.Success) {
                            getMessages(state.config)
                        }
                    }.onError {
                        Log.e(TAG, "Error upserting mailbox message in prepareAndStoreSubject: $it")
                    }
                }
            } catch (e: Exception) {
                Log.e(TAG, "Error preparing transaction for summary: $uri", e)
            } finally {
                withContext(Dispatchers.Main) {
                    onReady()
                }
            }
        }
    }

    fun upsertMessage(message: MailboxMessageRecord) {
        scope.launch {
            api.request<Unit>("upsertMailboxMessage") {
                put("message", JSONObject(BackendManager.json.encodeToString(message)))
            }.onSuccess {
                val state = _mailbox.value
                if (state is MailboxResult.Success) {
                    getMessages(state.config)
                }
            }.onError {
                Log.e(TAG, "Error upserting mailbox message: $it")
            }
        }
    }

    fun loadMailbox(baseUrl: String = "https://mailbox.gnunet.org") {
        _mailbox.value = MailboxResult.Loading
        scope.launch {
            Log.e(TAG, "Loading mailbox")
            api.request("getMailbox", MailboxResponse.serializer()) {
                put("mailboxBaseUrl", baseUrl)
            }.onSuccess { resp ->
                if (null == resp.mailboxConfiguration) {
                    initializeMailbox(baseUrl)
                } else {
                    Log.d(TAG, "loaded mailbox at ${resp.mailboxConfiguration.mailboxBaseUrl}")
                    getMessages(resp.mailboxConfiguration)
                }
            }.onError {
                initializeMailbox(baseUrl)
            }
        }
    }

    private fun initializeMailbox(baseUrl: String) {
        scope.launch {
            api.request("initializeMailbox", MailboxResponse.serializer()) {
                put("mailboxBaseUrl", baseUrl)
            }.onSuccess { resp ->
                val config = resp.mailboxConfiguration
                if (config != null) {
                    getMessages(config)
                } else {
                    Log.e(TAG, "Error initializing mailbox: missing configuration")
                    _mailbox.value = MailboxResult.Error(TalerErrorInfo(code = TalerErrorCode.UNKNOWN, message = "Missing mailbox configuration"))
                }
            }.onError {
                Log.e(TAG, "Error initializing mailbox: $it")
                _mailbox.value = MailboxResult.Error(it)
            }
        }
    }

    private fun getMessages(config: MailboxConfiguration) {
        scope.launch {
            api.request("getMailboxMessages", MailboxMessagesResponse.serializer())
                .onSuccess { response ->
                    _mailbox.value = MailboxResult.Success(config, response.messages)
                }.onError {
                    Log.e(TAG, "Error getting mailbox messages: $it")
                    _mailbox.value = MailboxResult.Error(it)
                }
        }
    }

    fun refreshMailbox(config: MailboxConfiguration) {
        Log.d(TAG, "refreshing mailbox at ${config.mailboxBaseUrl}")
        scope.launch {
            api.request("refreshMailbox", MailboxRefreshResponse.serializer()) {
                put("mailboxConf", JSONObject(BackendManager.json.encodeToString(config)))
            }.onSuccess { response ->
                val updatedConfig = response.mailboxExpiration?.let { exp ->
                    config.copy(expiration = exp)
                } ?: config
                if (response.messages.isNotEmpty()) {
                    response.messages.forEach { msg ->
                        addMessage(msg)
                    }
                    onMessage?.invoke("Received ${response.messages.size} new message(s)", "View") {
                        // The UI should handle navigation when this is called
                    }
                }
                getMessages(updatedConfig)
            }.onError {
                Log.e(TAG, "Error refreshing mailbox: $it")
            }
        }
    }

    private fun addMessage(message: MailboxMessageRecord) {
        scope.launch {
            api.request<Unit>("upsertMailboxMessage") {
                put("message", JSONObject(BackendManager.json.encodeToString(message)))
            }.onError {
                Log.e(TAG, "Error adding mailbox message: $it")
            }
        }
    }

    fun deleteMessage(config: MailboxConfiguration, message: MailboxMessageRecord) {
        scope.launch {
            api.request<Unit>("deleteMailboxMessage") {
                put("message", JSONObject(BackendManager.json.encodeToString(message)))
            }.onSuccess {
                getMessages(config)
            }.onError {
                Log.e(TAG, "Error deleting mailbox message: $it")
            }
        }
    }

    fun sendMessage(contact: net.taler.wallet.contacts.ContactEntry, talerUri: String) {
        scope.launch {
            api.request<Unit>("sendTalerUriMailboxMessage") {
                put("contact", JSONObject(BackendManager.json.encodeToString(contact)))
                put("talerUri", talerUri)
            }.onSuccess {
                Log.i(TAG, "Message sent successfully to ${contact.petname}")
                onMessage?.invoke("Message sent to ${contact.petname}", null, null)
            }.onError {
                Log.e(TAG, "Error sending message to ${contact.petname}: $it")
            }
        }
    }
}