#include "configuration.h"
#if HAS_SCREEN || defined(MESHTASTIC_INCLUDE_NICHE_GRAPHICS)
#include "FSCommon.h"
#include "MessageStore.h"
#include "NodeDB.h"
#include "SPILock.h"
#include "SafeFile.h"
#include "Throttle.h"
#include "UptimeClock.h"
#include "gps/RTC.h"
#include "memory/MemAudit.h"
#include <cstring> // memcpy

#ifndef MESSAGE_TEXT_POOL_SIZE
#define MESSAGE_TEXT_POOL_SIZE (MAX_MESSAGES_SAVED * MAX_MESSAGE_SIZE)
#endif

// Default autosave interval 2 hours, override per device later with -DMESSAGE_AUTOSAVE_INTERVAL_SEC=300 (etc)
#ifndef MESSAGE_AUTOSAVE_INTERVAL_SEC
#define MESSAGE_AUTOSAVE_INTERVAL_SEC (2 * 60 * 60)
#endif

// Global message text pool and state
static char *g_messagePool = nullptr;
static size_t g_poolWritePos = 0;

// Reset pool (called on boot or clear)
static inline void resetMessagePool()
{
    if (!g_messagePool) {
        g_messagePool = static_cast<char *>(malloc(MESSAGE_TEXT_POOL_SIZE));
        if (!g_messagePool) {
            LOG_ERROR("MessageStore: Failed to allocate %d bytes for message pool", MESSAGE_TEXT_POOL_SIZE);
            memaudit::set("msgstore", 0);
            return;
        }
        memaudit::set("msgstore", MESSAGE_TEXT_POOL_SIZE);
    }
    g_poolWritePos = 0;
    memset(g_messagePool, 0, MESSAGE_TEXT_POOL_SIZE);
}

// Allocate text in pool and return offset
// If not enough space remains, wrap around (ring buffer style)
static inline uint16_t storeTextInPool(const char *src, size_t len)
{
    // Pool allocation can fail at boot; getTextFromPool() already maps offset 0 to "" in that case
    if (!g_messagePool)
        return 0;

    if (len >= MAX_MESSAGE_SIZE)
        len = MAX_MESSAGE_SIZE - 1;

    // Wrap pool if out of space
    if (g_poolWritePos + len + 1 >= MESSAGE_TEXT_POOL_SIZE) {
        g_poolWritePos = 0;
    }

    uint16_t offset = g_poolWritePos;
    memcpy(&g_messagePool[g_poolWritePos], src, len);
    g_messagePool[g_poolWritePos + len] = '\0';
    g_poolWritePos += (len + 1);
    return offset;
}

// Retrieve a const pointer to message text by offset
static inline const char *getTextFromPool(uint16_t offset)
{
    if (!g_messagePool || offset >= MESSAGE_TEXT_POOL_SIZE)
        return "";
    return &g_messagePool[offset];
}

static inline bool isIgnoredNodeNum(uint32_t nodeNum)
{
    if (nodeNum == 0 || nodeNum == NODENUM_BROADCAST)
        return false;

    const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeNum);
    return nodeInfoLiteIsIgnored(node);
}

// Helper: assign a timestamp (RTC if available, else boot-relative)
static inline void assignTimestamp(StoredMessage &sm)
{
    uint32_t nowSecs = getValidTime(RTCQuality::RTCQualityDevice, true);
    if (nowSecs) {
        sm.timestamp = nowSecs;
        sm.isBootRelative = false;
    } else {
        // Uptime seconds, not millis()/1000: a stamp taken before the 32-bit wrap otherwise reads as
        // newer than "now" afterwards, and upgradeBootRelativeTimestamps() then declines to heal it.
        sm.timestamp = Time::getUptimeSecs();
        sm.isBootRelative = true;
    }
}

// Generic push with cap (used by live + persisted queues)
template <typename T> static inline void pushWithLimit(std::deque<T> &queue, const T &msg)
{
    if (queue.size() >= MAX_MESSAGES_SAVED)
        queue.pop_front();
    queue.push_back(msg);
}

template <typename T> static inline void pushWithLimit(std::deque<T> &queue, T &&msg)
{
    if (queue.size() >= MAX_MESSAGES_SAVED)
        queue.pop_front();
    queue.emplace_back(std::move(msg));
}

MessageStore::MessageStore(const std::string &label)
{
    filename = "/Messages_" + label + ".msgs";
    resetMessagePool(); // initialize text pool on boot
}

// Live message handling (RAM only)
void MessageStore::addLiveMessage(StoredMessage &&msg)
{
    pushWithLimit(liveMessages, std::move(msg));
}
void MessageStore::addLiveMessage(const StoredMessage &msg)
{
    pushWithLimit(liveMessages, msg);
}

#if ENABLE_MESSAGE_PERSISTENCE
static bool g_messageStoreHasUnsavedChanges = false;
static uint32_t g_lastAutoSaveMs = 0; // last time we actually saved

static inline uint32_t autosaveIntervalMs()
{
    uint32_t sec = (uint32_t)MESSAGE_AUTOSAVE_INTERVAL_SEC;
    if (sec < 60)
        sec = 60;
    return sec * 1000UL;
}

// Mark new messages in RAM that need to be saved later
static inline void markMessageStoreUnsaved()
{
    g_messageStoreHasUnsavedChanges = true;

    if (g_lastAutoSaveMs == 0) {
        g_lastAutoSaveMs = Time::getMillis();
    }
}

// Called periodically from the main loop in main.cpp
static inline void autosaveTick(MessageStore *store)
{
    if (!store)
        return;

    uint32_t now = Time::getMillis();

    if (g_lastAutoSaveMs == 0) {
        g_lastAutoSaveMs = now;
        return;
    }

    if (Throttle::isWithinTimespanMs(g_lastAutoSaveMs, autosaveIntervalMs()))
        return;

    // Autosave interval reached, only save if there are unsaved messages.
    if (g_messageStoreHasUnsavedChanges) {
        LOG_INFO("Autosaving MessageStore to flash");
        store->saveToFlash();
    } else {
        LOG_INFO("Autosave skipped, no changes to save");
        g_lastAutoSaveMs = now;
    }
}
#endif

bool MessageStore::shouldStorePacket(const meshtastic_MeshPacket &packet) const
{
    const uint32_t localNode = nodeDB->getNodeNum();
    const bool isDM = packet.to != 0 && packet.to != NODENUM_BROADCAST;
    if (isDM) {
        const bool outgoing = packet.from == 0 || packet.from == localNode;
        const uint32_t peer = outgoing ? packet.to : packet.from;
        return !isIgnoredNodeNum(peer);
    }

    if (packet.from != 0 && packet.from != localNode)
        return !isIgnoredNodeNum(packet.from);

    return true;
}

bool MessageStore::isMessageVisible(const StoredMessage &msg) const
{
    const uint32_t localNode = nodeDB->getNodeNum();
    if (msg.type == MessageType::DM_TO_US) {
        const uint32_t peer = (msg.sender == localNode) ? msg.dest : msg.sender;
        return !isIgnoredNodeNum(peer);
    }

    if (msg.sender != 0 && msg.sender != localNode)
        return !isIgnoredNodeNum(msg.sender);

    return true;
}

// Add from incoming/outgoing packet
const StoredMessage *MessageStore::tryAddFromPacket(const meshtastic_MeshPacket &packet)
{
    if (!shouldStorePacket(packet)) {
        LOG_DEBUG("Drop store 0x%08x", packet.from);
        return nullptr;
    }

    StoredMessage sm;
    assignTimestamp(sm);
    sm.channelIndex = packet.channel;

    const char *payload = reinterpret_cast<const char *>(packet.decoded.payload.bytes);
    // payload.bytes is not NUL-terminated, so bound by the received size too: a shorter message
    // stored after a longer one would otherwise pick up the previous occupant's trailing bytes.
    size_t avail = packet.decoded.payload.size;
    if (avail > MAX_MESSAGE_SIZE - 1)
        avail = MAX_MESSAGE_SIZE - 1;
    size_t len = strnlen(payload, avail);
    sm.textOffset = storeTextInPool(payload, len);
    sm.textLength = len;

    // Determine sender
    uint32_t localNode = nodeDB->getNodeNum();
    sm.sender = (packet.from == 0) ? localNode : packet.from;

    sm.dest = packet.to;

    bool isDM = (sm.dest != 0 && sm.dest != NODENUM_BROADCAST);

    sm.type = isDM ? MessageType::DM_TO_US : MessageType::BROADCAST;
    sm.ackStatus = (packet.from == 0) ? AckStatus::NONE : AckStatus::ACKED;

#if !(MESHTASTIC_EXCLUDE_PKI_KEYGEN || MESHTASTIC_EXCLUDE_PKI)
    sm.xeddsaSigned = packet.xeddsa_signed;
#endif

    addLiveMessage(sm);

#if ENABLE_MESSAGE_PERSISTENCE
    markMessageStoreUnsaved();
#endif

    return &liveMessages.back();
}

#if ENABLE_MESSAGE_PERSISTENCE

// Compact, fixed-size on-flash representation using offset + length
struct __attribute__((packed)) StoredMessageRecord {
    uint32_t timestamp;
    uint32_t sender;
    uint8_t channelIndex;
    uint32_t dest;
    uint8_t isBootRelative;
    uint8_t ackStatus;           // static_cast<uint8_t>(AckStatus)
    uint8_t type;                // static_cast<uint8_t>(MessageType)
    uint8_t xeddsaSigned;        // 1 if packet carried a verified XEdDSA signature
    uint16_t textLength;         // message length
    char text[MAX_MESSAGE_SIZE]; // store actual text here
};

// Serialize one StoredMessage to flash
static inline void writeMessageRecord(SafeFile &f, const StoredMessage &m)
{
    StoredMessageRecord rec = {};
    rec.timestamp = m.timestamp;
    rec.sender = m.sender;
    rec.channelIndex = m.channelIndex;
    rec.dest = m.dest;
    rec.isBootRelative = m.isBootRelative;
    rec.ackStatus = static_cast<uint8_t>(m.ackStatus);
    rec.type = static_cast<uint8_t>(m.type);
    rec.xeddsaSigned = m.xeddsaSigned ? 1 : 0;
    rec.textLength = m.textLength;

    // Copy the actual text into the record from RAM pool
    const char *txt = getTextFromPool(m.textOffset);
    strncpy(rec.text, txt, MAX_MESSAGE_SIZE - 1);
    rec.text[MAX_MESSAGE_SIZE - 1] = '\0';

    f.write(reinterpret_cast<const uint8_t *>(&rec), sizeof(rec));
}

// Deserialize one StoredMessage from flash; returns false on short read
static inline bool readMessageRecord(File &f, StoredMessage &m)
{
    StoredMessageRecord rec = {};
    if (f.readBytes(reinterpret_cast<char *>(&rec), sizeof(rec)) != sizeof(rec))
        return false;

    m.timestamp = rec.timestamp;
    m.sender = rec.sender;
    m.channelIndex = rec.channelIndex;
    m.dest = rec.dest;
    m.isBootRelative = rec.isBootRelative;
    m.ackStatus = static_cast<AckStatus>(rec.ackStatus);
    m.type = static_cast<MessageType>(rec.type);
    m.xeddsaSigned = rec.xeddsaSigned != 0;
    m.textLength = rec.textLength;

    // 💡 Re-store text into pool and update offset
    m.textLength = strnlen(rec.text, MAX_MESSAGE_SIZE - 1);
    m.textOffset = storeTextInPool(rec.text, m.textLength);

    return true;
}

void MessageStore::saveToFlash()
{
#ifdef FSCom
    // Ensure root exists
    spiLock->lock();
    FSCom.mkdir("/");
    spiLock->unlock();

    SafeFile f(filename.c_str(), false);

    spiLock->lock();
    uint8_t count = static_cast<uint8_t>(liveMessages.size());
    if (count > MAX_MESSAGES_SAVED)
        count = MAX_MESSAGES_SAVED;
    f.write(&count, 1);

    for (uint8_t i = 0; i < count; ++i) {
        writeMessageRecord(f, liveMessages[i]);
    }
    spiLock->unlock();

    f.close();
#endif

    // Reset autosave state after any save
    g_messageStoreHasUnsavedChanges = false;
    g_lastAutoSaveMs = Time::getMillis();
}

void MessageStore::loadFromFlash()
{
    std::deque<StoredMessage>().swap(liveMessages);
    resetMessagePool(); // reset pool when loading

#ifdef FSCom
    {
        concurrency::LockGuard guard(spiLock);

        if (!FSCom.exists(filename.c_str()))
            return;

        auto f = FSCom.open(filename.c_str(), FILE_O_READ);
        if (!f)
            return;

        uint8_t count = 0;
        f.readBytes(reinterpret_cast<char *>(&count), 1);
        if (count > MAX_MESSAGES_SAVED)
            count = MAX_MESSAGES_SAVED;

        for (uint8_t i = 0; i < count; ++i) {
            StoredMessage m;
            if (!readMessageRecord(f, m))
                break;
            liveMessages.push_back(m);
        }

        f.close();
    }

    if (pruneHiddenMessages())
        saveToFlash();
#endif
    // Loading messages does not trigger an autosave
    g_messageStoreHasUnsavedChanges = false;
    g_lastAutoSaveMs = Time::getMillis();
}

#else
// If persistence is disabled, these functions become no-ops
void MessageStore::saveToFlash() {}
void MessageStore::loadFromFlash() {}
#endif

// Clear all messages (RAM + persisted queue)
void MessageStore::clearAllMessages()
{
    std::deque<StoredMessage>().swap(liveMessages);
    resetMessagePool();

#ifdef FSCom
    SafeFile f(filename.c_str(), false);
    uint8_t count = 0;

    // SafeFile already does its own spiLock in its constructor and close().
    // Avoid nesting spiLocks, as this will hang until watchdog reset!
    {
        concurrency::LockGuard guard(spiLock);
        f.write(&count, 1); // write "0 messages"
    }

    f.close();
#endif

#if ENABLE_MESSAGE_PERSISTENCE
    g_messageStoreHasUnsavedChanges = false;
    g_lastAutoSaveMs = Time::getMillis();
#endif
}

// Internal helpers for targeted erasure.
template <typename Predicate> static bool eraseFirstMatch(std::deque<StoredMessage> &deque, Predicate pred)
{
    for (auto it = deque.begin(); it != deque.end(); ++it) {
        if (pred(*it)) {
            deque.erase(it);
            return true;
        }
    }
    return false;
}

template <typename Predicate> static void eraseAllMatches(std::deque<StoredMessage> &deque, Predicate pred)
{
    for (auto it = deque.begin(); it != deque.end();) {
        if (pred(*it)) {
            it = deque.erase(it);
        } else {
            ++it;
        }
    }
}

bool MessageStore::pruneHiddenMessages()
{
    const size_t before = liveMessages.size();
    eraseAllMatches(liveMessages, [&](const StoredMessage &m) { return !isMessageVisible(m); });
    return liveMessages.size() != before;
}

// Delete oldest message (RAM + persisted queue)
void MessageStore::deleteOldestMessage()
{
    if (!liveMessages.empty()) {
        liveMessages.pop_front();
    }
    saveToFlash();
}

// Delete oldest message in a specific channel
void MessageStore::deleteOldestMessageInChannel(uint8_t channel)
{
    auto pred = [channel](const StoredMessage &m) { return m.type == MessageType::BROADCAST && m.channelIndex == channel; };
    eraseFirstMatch(liveMessages, pred);
    saveToFlash();
}

void MessageStore::deleteAllMessagesInChannel(uint8_t channel)
{
    auto pred = [channel](const StoredMessage &m) { return m.type == MessageType::BROADCAST && m.channelIndex == channel; };
    eraseAllMatches(liveMessages, pred);
    saveToFlash();
}

void MessageStore::deleteAllMessagesWithPeer(uint32_t peer)
{
    uint32_t local = nodeDB->getNodeNum();
    auto pred = [&](const StoredMessage &m) {
        if (m.type != MessageType::DM_TO_US)
            return false;
        uint32_t other = (m.sender == local) ? m.dest : m.sender;
        return other == peer;
    };
    eraseAllMatches(liveMessages, pred);
    saveToFlash();
}

void MessageStore::deleteAllMessagesFromNode(uint32_t nodeNum)
{
    const uint32_t local = nodeDB->getNodeNum();
    auto pred = [&](const StoredMessage &m) {
        if (m.sender == nodeNum)
            return true;
        if (m.type != MessageType::DM_TO_US)
            return false;
        return m.sender == local ? m.dest == nodeNum : m.sender == nodeNum;
    };
    eraseAllMatches(liveMessages, pred);
    saveToFlash();
}

// Delete oldest message in a direct chat with a node
void MessageStore::deleteOldestMessageWithPeer(uint32_t peer)
{
    auto pred = [peer](const StoredMessage &m) {
        if (m.type != MessageType::DM_TO_US)
            return false;
        uint32_t other = (m.sender == nodeDB->getNodeNum()) ? m.dest : m.sender;
        return other == peer;
    };
    eraseFirstMatch(liveMessages, pred);
    saveToFlash();
}

std::deque<StoredMessage> MessageStore::getChannelMessages(uint8_t channel) const
{
    std::deque<StoredMessage> result;
    for (const auto &m : liveMessages) {
        if (isMessageVisible(m) && m.type == MessageType::BROADCAST && m.channelIndex == channel) {
            result.push_back(m);
        }
    }
    return result;
}

std::deque<StoredMessage> MessageStore::getDirectMessages() const
{
    std::deque<StoredMessage> result;
    for (const auto &m : liveMessages) {
        if (isMessageVisible(m) && m.type == MessageType::DM_TO_US) {
            result.push_back(m);
        }
    }
    return result;
}

bool MessageStore::hasVisibleMessages() const
{
    for (const auto &m : liveMessages) {
        if (isMessageVisible(m))
            return true;
    }
    return false;
}

// Upgrade boot-relative timestamps once RTC is valid
// Only same-boot boot-relative messages are healed.
// Persisted boot-relative messages from old boots stay ??? forever.
void MessageStore::upgradeBootRelativeTimestamps()
{
    uint32_t nowSecs = getValidTime(RTCQuality::RTCQualityDevice, true);
    if (nowSecs == 0)
        return; // Still no valid RTC

    uint32_t bootNow = Time::getUptimeSecs();

    auto fix = [&](std::deque<StoredMessage> &dq) {
        for (auto &m : dq) {
            if (m.isBootRelative && m.timestamp <= bootNow) {
                uint32_t bootOffset = nowSecs - bootNow;
                m.timestamp += bootOffset;
                m.isBootRelative = false;
            }
        }
    };
    fix(liveMessages);
}

const char *MessageStore::getText(const StoredMessage &msg)
{
    // Wrapper around the internal helper
    return getTextFromPool(msg.textOffset);
}

uint16_t MessageStore::storeText(const char *src, size_t len)
{
    // Wrapper around the internal helper
    return storeTextInPool(src, len);
}

#if ENABLE_MESSAGE_PERSISTENCE
void messageStoreAutosaveTick()
{
    // Called from the main loop to check autosave timing
    autosaveTick(&messageStore);
}
#endif

// Global definition
MessageStore messageStore("default");
#endif
