// DragonX Wallet - HushChat service (implementation).

#include "chat_service.h"

#include "chat_database.h"
#include "chat_identity.h"  // chatIdentityPublicKeyHex

#include <utility>

namespace dragonx::chat {

ChatService::~ChatService() {
    clearIdentity();
}

void ChatService::setIdentity(const ChatKeyPair& keys) {
    identity_ = keys;
    has_identity_ = true;
}

void ChatService::clearIdentity() {
    wipeChatKeyPair(identity_);
    has_identity_ = false;
}

int ChatService::ingest(const std::vector<HushChatTransactionMetadata>& metadata,
                        const std::unordered_map<std::string, std::int64_t>& txTimestamps,
                        std::int64_t fallbackTimestamp,
                        std::vector<std::string>* newIncomingCids) {
    if (!has_identity_) return 0;
    // Persistence is attached but not unlocked (e.g. the DB failed to open with the seed): we can't
    // consult tombstones, so ingesting now would resurface locally-deleted messages into the store.
    // Skip until the DB is usable — chat is degraded anyway without its store.
    if (db_ && !db_->hasKey()) return 0;

    const std::string myPubKey = chatIdentityPublicKeyHex(identity_);

    int added = 0;
    for (const auto& meta : metadata) {
        // A memo whose sender is our OWN identity is something we sent (only we hold our key). The local
        // echo already records it as outgoing — ingesting it as incoming would duplicate it as a phantom
        // "from peer" message. (This also collapses same-seed self-chat, where the "peer" wallet shares
        // our identity, so every message would otherwise loop back.)
        if (!myPubKey.empty() && meta.sender_public_key_hex == myPubKey) continue;

        ChatMessage message;
        message.direction = ChatDirection::Incoming;
        message.txid = meta.txid;
        message.conversation_id = meta.conversation_id;
        message.peer_zaddr = meta.reply_zaddr;
        message.peer_public_key_hex = meta.sender_public_key_hex;
        // Reference time: the tx/receive time (block time if confirmed, else the receiver's wall clock for
        // a mempool receive).
        const auto timeIt = txTimestamps.find(meta.txid);
        const std::int64_t refTime = timeIt != txTimestamps.end() ? timeIt->second : fallbackTimestamp;
        // Prefer the sender's stamped compose time (header "ts") — the true send time, shown identically on
        // both ends. But REJECT a value implausibly in the FUTURE vs the reference: a wrong/ahead peer clock
        // would otherwise pin their messages to the bottom of the thread forever. A compose time in the
        // PAST is fine — the note buffer can broadcast a queued message long after it was composed, and a
        // confirmed tx's block time is always >= the compose time.
        constexpr std::int64_t kSenderTsFutureToleranceSec = 3600;  // 1 hour of clock skew tolerated
        if (meta.sent_at > 0 && (refTime <= 0 || meta.sent_at <= refTime + kSenderTsFutureToleranceSec)) {
            message.timestamp = meta.sent_at;
        } else {
            message.timestamp = refTime;
        }
        message.payload_position = meta.payload_position;

        // Suppress locally-removed conversations before the (relatively costly) decrypt. A blocked cid
        // is dropped outright (old + future messages) until unblocked; a tombstoned (txid,position) was
        // deleted with "revive on new message", so only that exact message is skipped — a new message
        // in the same conversation has a different txid and flows through normally.
        if (blocked_pred_ && blocked_pred_(message.conversation_id)) continue;
        if (db_ && db_->isTombstoned(message)) continue;

        if (meta.type == HushChatHeaderType::ContactRequest) {
            message.kind = ChatMessageKind::ContactRequest;
            message.body = meta.payload_memo;  // plaintext request text
        } else {
            message.kind = ChatMessageKind::Message;
            std::string plaintext;
            const ChatCryptoStatus status = decryptIncoming(
                identity_, meta.sender_public_key_hex, meta.secretstream_header_hex,
                meta.payload_memo, plaintext);
            if (status != ChatCryptoStatus::Ok) continue;  // drop undecryptable silently
            message.body = std::move(plaintext);
        }

        // In-memory store dedups (txid+position); only persist the genuinely new ones. On the next
        // session loadFromDatabase() repopulates the store, so re-scanning the chain re-ingests but
        // the store dedup prevents a duplicate write.
        if (store_.append(message)) {
            if (db_) db_->append(message);
            ++added;
            // Every ingested message is incoming — report its cid so the caller can notify without
            // relying on a seen-watermark delta (which block-time vs wall-clock skew can swallow).
            if (newIncomingCids) newIncomingCids->push_back(message.conversation_id);
        }
    }
    return added;
}

void ChatService::loadFromDatabase() {
    if (!db_) return;
    for (const auto& message : db_->load()) {
        // Never surface a blocked conversation, even if a prior "delete & block" failed to remove its
        // rows (defense-in-depth): the ingest guard already drops live scans, this covers the reload path.
        if (blocked_pred_ && blocked_pred_(message.conversation_id)) continue;
        store_.append(message);
    }
}

bool ChatService::deleteConversation(const std::string& conversationId, bool block) {
    // Delete the persisted rows FIRST and only mutate the in-memory view if that succeeds. Doing it the
    // other way round means a failed DB write (disk full / locked) would empty the store while the rows
    // survive — and on the next reload the conversation silently reappears with no tombstone.
    // Revive-on-new-message => tombstone the removed rows so a re-scan won't re-import them.
    // Block => remove the rows without a tombstone; the caller's blocked predicate suppresses re-import
    // until unblocked, at which point the conversation re-imports from chain.
    if (db_) {
        std::vector<ChatMessage> msgs = store_.conversation(conversationId);  // snapshot (copy)
        if (!db_->deleteMessages(msgs, /*tombstone=*/!block)) return false;
    }
    store_.eraseConversation(conversationId);
    return true;
}

std::string ChatService::identityPublicKeyHex() const {
    if (!has_identity_) return {};
    return chatIdentityPublicKeyHex(identity_);
}

ChatComposeStatus ChatService::composeMessage(const std::string& myReplyZaddr,
                                              const std::string& peerPublicKeyHex,
                                              const std::string& peerZaddr,
                                              const std::string& conversationId,
                                              const std::string& plaintext,
                                              OutgoingChatMemos& out) const {
    if (!has_identity_) return ChatComposeStatus::MissingField;
    return buildOutgoingMessage(identity_, chatIdentityPublicKeyHex(identity_), myReplyZaddr,
                                peerPublicKeyHex, peerZaddr, conversationId, plaintext, out);
}

ChatComposeStatus ChatService::composeContactRequest(const std::string& myReplyZaddr,
                                                     const std::string& peerZaddr,
                                                     const std::string& conversationId,
                                                     const std::string& requestText,
                                                     OutgoingChatMemos& out) const {
    if (!has_identity_) return ChatComposeStatus::MissingField;
    return buildOutgoingContactRequest(chatIdentityPublicKeyHex(identity_), myReplyZaddr,
                                       peerZaddr, conversationId, requestText, out);
}

bool ChatService::recordOutgoing(const ChatMessage& message) {
    if (store_.append(message)) {
        if (db_) db_->append(message);
        return true;
    }
    return false;
}

bool ChatService::recordOutgoingPending(const ChatMessage& message) {
    // Persist immediately (as Sending) so a send survives an app quit before the broadcast resolves;
    // resolveOutgoing() then UPSERTS the row to the final status. A stray persisted Sending (crash mid-
    // broadcast) loads as Sent (see ChatDatabase::deserialize).
    const bool appended = store_.append(message);
    if (appended && db_) db_->upsert(message);
    return appended;
}

void ChatService::resolveOutgoing(const std::string& txid, ChatDelivery delivery) {
    const ChatMessage* updated = store_.updateDelivery(txid, delivery);
    if (updated && db_) db_->upsert(*updated);  // overwrite the Sending row with the final status
}

} // namespace dragonx::chat
