#include "NeighborInfoModule.h"
#include "Default.h"
#include "MeshService.h"
#include "NodeDB.h"
#include "gps/RTC.h"
#include <Throttle.h>

NeighborInfoModule *neighborInfoModule;

/*
Prints a single neighbor info packet and associated neighbors
Uses LOG_DEBUG, which equates to Console.log
NOTE: For debugging only
*/
void NeighborInfoModule::printNeighborInfo(const char *header, const meshtastic_NeighborInfo *np)
{
    LOG_TRACE("%s NEIGHBORINFO PACKET from Node 0x%08x to Node 0x%08x (last sent by 0x%08x)", header, np->node_id,
              nodeDB->getNodeNum(), np->last_sent_by_id);
    LOG_TRACE("Packet contains %d neighbors", np->neighbors_count);
    for (int i = 0; i < np->neighbors_count; i++) {
        LOG_TRACE("Neighbor %d: node_id=0x%08x, snr=%.2f", i, np->neighbors[i].node_id, np->neighbors[i].snr);
    }
}

/*
Prints the nodeDB neighbors
NOTE: for debugging only
*/
void NeighborInfoModule::printNodeDBNeighbors()
{
    LOG_TRACE("Our NodeDB contains %u neighbors", (unsigned)neighbors.size());
    for (size_t i = 0; i < neighbors.size(); i++) {
        LOG_TRACE("Node %u: node_id=0x%08x, snr=%.2f", (unsigned)i, neighbors[i].node_id, neighbors[i].snr);
    }
}

/* Send our initial owner announcement 35 seconds after we start (to give
 * network time to setup) */
NeighborInfoModule::NeighborInfoModule()
    : ProtobufModule("neighborinfo", meshtastic_PortNum_NEIGHBORINFO_APP, &meshtastic_NeighborInfo_msg),
      concurrency::OSThread("NeighborInfo")
{
    ourPortNum = meshtastic_PortNum_NEIGHBORINFO_APP;
    nodeStatusObserver.observe(&nodeStatus->onNewStatus);

    if (moduleConfig.neighbor_info.enabled) {
        isPromiscuous = true; // Update neighbors from all packets
        setIntervalFromNow(Default::getConfiguredOrDefaultMs(moduleConfig.neighbor_info.update_interval,
                                                             default_telemetry_broadcast_interval_secs));
    } else {
        LOG_DEBUG("NeighborInfoModule is disabled");
        disable();
    }
}

/*
Collect neighbor info from the nodeDB's history, capping at a maximum number of
entries and max time Assumes that the neighborInfo packet has been allocated
@returns the number of entries collected
*/
uint32_t NeighborInfoModule::collectNeighborInfo(meshtastic_NeighborInfo *neighborInfo)
{
    NodeNum my_node_id = nodeDB->getNodeNum();
    neighborInfo->node_id = my_node_id;
    neighborInfo->last_sent_by_id = my_node_id;
    neighborInfo->node_broadcast_interval_secs =
        Default::getConfiguredOrDefault(moduleConfig.neighbor_info.update_interval, default_telemetry_broadcast_interval_secs);

    cleanUpNeighbors();

    for (auto nbr : neighbors) {
        if ((neighborInfo->neighbors_count < MAX_NUM_NEIGHBORS) && (nbr.node_id != my_node_id)) {
            neighborInfo->neighbors[neighborInfo->neighbors_count].node_id = nbr.node_id;
            neighborInfo->neighbors[neighborInfo->neighbors_count].snr = nbr.snr;
            // Note: we don't set the last_rx_time and node_broadcast_intervals_secs
            // here, because we don't want to send this over the mesh
            neighborInfo->neighbors_count++;
        }
    }
    printNodeDBNeighbors();
    return neighborInfo->neighbors_count;
}

/*
  Remove neighbors from the database that we haven't heard from in a while
*/
void NeighborInfoModule::cleanUpNeighbors()
{
    uint32_t now = getTime();
    NodeNum my_node_id = nodeDB->getNodeNum();
    for (auto it = neighbors.rbegin(); it != neighbors.rend();) {
        // We will remove a neighbor if we haven't heard from them in twice the
        // broadcast interval cannot use isWithinTimespanMs() as it->last_rx_time is
        // seconds since 1970
        if ((now - it->last_rx_time > it->node_broadcast_interval_secs * 2) && (it->node_id != my_node_id)) {
            LOG_DEBUG("Remove neighbor with node ID 0x%08x", it->node_id);
            it = std::vector<meshtastic_Neighbor>::reverse_iterator(
                neighbors.erase(std::next(it).base())); // Erase the element and update the iterator
        } else {
            ++it;
        }
    }
}

/* Send neighbor info to the mesh */
void NeighborInfoModule::sendNeighborInfo(NodeNum dest, bool wantReplies)
{
    meshtastic_NeighborInfo neighborInfo = meshtastic_NeighborInfo_init_zero;
    collectNeighborInfo(&neighborInfo);
    // only send neighbours if we have some to send
    if (neighborInfo.neighbors_count > 0) {
        meshtastic_MeshPacket *p = allocDataProtobuf(neighborInfo);
        if (!p)
            return;
        p->to = dest;
        p->decoded.want_response = wantReplies;
        p->priority = meshtastic_MeshPacket_Priority_BACKGROUND;
        printNeighborInfo("SENDING", &neighborInfo);
        service->sendToMesh(p, RX_SRC_LOCAL, true);
    }
}

/*
Encompasses the full construction and sending packet to mesh
Will be used for broadcast.
*/
int32_t NeighborInfoModule::runOnce()
{
    if (moduleConfig.neighbor_info.transmit_over_lora &&
        (!channels.isDefaultChannel(channels.getPrimaryIndex()) || !RadioInterface::uses_default_frequency_slot) &&
        airTime->isTxAllowedChannelUtil(true) && airTime->isTxAllowedAirUtil()) {
        sendNeighborInfo(NODENUM_BROADCAST, false);
    } else {
        sendNeighborInfo(NODENUM_BROADCAST_NO_LORA, false);
    }
    return Default::getConfiguredOrDefaultMs(moduleConfig.neighbor_info.update_interval, default_neighbor_info_broadcast_secs);
}

meshtastic_MeshPacket *NeighborInfoModule::allocReply()
{
    LOG_INFO("NeighborInfoRequested");
    if (lastSentReply && Throttle::isWithinTimespanMs(lastSentReply, 3 * 60 * 1000)) {
        LOG_DEBUG("Skip Neighbors reply since we sent a reply <3min ago");
        ignoreRequest = true; // Mark it as ignored for MeshModule
        return nullptr;
    }

    meshtastic_NeighborInfo neighborInfo = meshtastic_NeighborInfo_init_zero;
    collectNeighborInfo(&neighborInfo);

    meshtastic_MeshPacket *reply = allocDataProtobuf(neighborInfo);

    if (reply) {
        lastSentReply = millis(); // Track when we sent this reply
    }
    return reply;
}

/*
Collect a received neighbor info packet from another node
Pass it to an upper client; do not persist this data on the mesh
*/
bool NeighborInfoModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_NeighborInfo *np)
{
    LOG_TRACE("NeighborInfo: handleReceivedProtobuf");
    if (np) {
        printNeighborInfo("RECEIVED", np);
        // Ignore dummy/interceptable packets: single neighbor with nodeId 0 and snr 0
        if (np->neighbors_count != 1 || np->neighbors[0].node_id != 0 || np->neighbors[0].snr != 0.0f) {
            LOG_TRACE("  Updating neighbours");
            updateNeighbors(mp, np);
        } else {
            LOG_DEBUG("  Ignoring dummy neighbor info packet (single neighbor with nodeId 0, snr 0)");
        }
    } else if (getHopsAway(mp) == 0) {
        LOG_TRACE("Get or create neighbor: 0x%08x with snr %f", mp.from, mp.rx_snr);
        // If the hopLimit is the same as hopStart, then it is a neighbor
        getOrCreateNeighbor(mp.from, mp.from, 0,
                            mp.rx_snr); // Set the broadcast interval to 0, as we don't know it
    }
    // Allow others to handle this packet
    return false;
}

/*
Copy the content of a current NeighborInfo packet into a new one and update the
last_sent_by_id to our NodeNum
*/
void NeighborInfoModule::alterReceivedProtobuf(meshtastic_MeshPacket &p, meshtastic_NeighborInfo *n)
{
    n->last_sent_by_id = nodeDB->getNodeNum();

    // Set updated last_sent_by_id to the payload of the to be flooded packet
    p.decoded.payload.size =
        pb_encode_to_bytes(p.decoded.payload.bytes, sizeof(p.decoded.payload.bytes), &meshtastic_NeighborInfo_msg, n);
}

void NeighborInfoModule::resetNeighbors()
{
    neighbors.clear();
}

void NeighborInfoModule::updateNeighbors(const meshtastic_MeshPacket &mp, const meshtastic_NeighborInfo *np)
{
    // The last sent ID will be 0 if the packet is from the phone, which we don't
    // count as an edge. So we assume that if it's zero, then this packet is from
    // our node.
    if (mp.which_payload_variant == meshtastic_MeshPacket_decoded_tag && mp.from) {
        getOrCreateNeighbor(mp.from, np->last_sent_by_id, np->node_broadcast_interval_secs, mp.rx_snr);
    }
}

meshtastic_Neighbor *NeighborInfoModule::getOrCreateNeighbor(NodeNum originalSender, NodeNum n,
                                                             uint32_t node_broadcast_interval_secs, float snr)
{
    // our node and the phone are the same node (not neighbors)
    if (n == 0) {
        n = nodeDB->getNodeNum();
    }
    // look for one in the existing list
    for (size_t i = 0; i < neighbors.size(); i++) {
        if (neighbors[i].node_id == n) {
            // if found, update it
            neighbors[i].snr = snr;
            neighbors[i].last_rx_time = getTime();
            // Only if this is the original sender, the broadcast interval corresponds
            // to it
            if (originalSender == n && node_broadcast_interval_secs != 0)
                neighbors[i].node_broadcast_interval_secs = node_broadcast_interval_secs;
            return &neighbors[i];
        }
    }
    // otherwise, allocate one and assign data to it

    meshtastic_Neighbor new_nbr = meshtastic_Neighbor_init_zero;
    new_nbr.node_id = n;
    new_nbr.snr = snr;
    new_nbr.last_rx_time = getTime();
    // Only if this is the original sender, the broadcast interval corresponds to
    // it
    if (originalSender == n && node_broadcast_interval_secs != 0)
        new_nbr.node_broadcast_interval_secs = node_broadcast_interval_secs;
    else // Assume the same broadcast interval as us for the neighbor if we don't
         // know it
        new_nbr.node_broadcast_interval_secs = moduleConfig.neighbor_info.update_interval;

    if (neighbors.size() < MAX_NUM_NEIGHBORS) {
        neighbors.push_back(new_nbr);
    } else {
        // If we have too many neighbors, replace the oldest one
        LOG_WARN("Neighbor DB is full, replace oldest neighbor");
        neighbors.erase(neighbors.begin());
        neighbors.push_back(new_nbr);
    }
    return &neighbors.back();
}
