#include "TraceRouteModule.h"
#include "MeshService.h"
#include "NodeDB.h"
#include "graphics/Screen.h"
#include "graphics/ScreenFonts.h"
#include "graphics/SharedUIDisplay.h"
#include "mesh/Router.h"
#include "meshUtils.h"
#include <vector>

#if HAS_TRAFFIC_MANAGEMENT
#include "modules/TrafficManagementModule.h"
#endif

extern std::unique_ptr<graphics::Screen> screen;

TraceRouteModule *traceRouteModule;

void TraceRouteModule::setResultText(const String &text)
{
    resultText = text;
    resultLines.clear();
    resultLinesDirty = true;
}

void TraceRouteModule::clearResultLines()
{
    resultLines.clear();
    resultLinesDirty = false;
}
#if HAS_SCREEN
void TraceRouteModule::rebuildResultLines(OLEDDisplay *display)
{
    if (!display) {
        resultLinesDirty = false;
        return;
    }

    resultLines.clear();

    if (resultText.length() == 0) {
        resultLinesDirty = false;
        return;
    }

    int maxWidth = display->getWidth() - 4;
    if (maxWidth <= 0) {
        resultLinesDirty = false;
        return;
    }

    int start = 0;
    int textLength = resultText.length();

    while (start <= textLength) {
        int newlinePos = resultText.indexOf('\n', start);
        String segment;

        if (newlinePos != -1) {
            segment = resultText.substring(start, newlinePos);
            start = newlinePos + 1;
        } else {
            segment = resultText.substring(start);
            start = textLength + 1;
        }

        if (segment.length() == 0) {
            resultLines.push_back("");
            continue;
        }

        if (display->getStringWidth(segment) <= maxWidth) {
            resultLines.push_back(segment);
            continue;
        }

        String remaining = segment;

        while (remaining.length() > 0) {
            String tempLine = "";
            int lastGoodBreak = -1;
            bool lineComplete = false;

            for (int i = 0; i < static_cast<int>(remaining.length()); i++) {
                char ch = remaining.charAt(i);
                String testLine = tempLine + ch;

                if (display->getStringWidth(testLine) > maxWidth) {
                    if (lastGoodBreak >= 0) {
                        resultLines.push_back(remaining.substring(0, lastGoodBreak + 1));
                        remaining = remaining.substring(lastGoodBreak + 1);
                        lineComplete = true;
                        break;
                    } else if (tempLine.length() > 0) {
                        resultLines.push_back(tempLine);
                        remaining = remaining.substring(i);
                        lineComplete = true;
                        break;
                    } else {
                        resultLines.push_back(String(ch));
                        remaining = remaining.substring(i + 1);
                        lineComplete = true;
                        break;
                    }
                } else {
                    tempLine = testLine;
                    if (ch == ' ' || ch == '>' || ch == '<' || ch == '-' || ch == '(' || ch == ')' || ch == ',') {
                        lastGoodBreak = i;
                    }
                }
            }

            if (!lineComplete) {
                if (tempLine.length() > 0) {
                    resultLines.push_back(tempLine);
                }
                break;
            }
        }
    }

    resultLinesDirty = false;
}
#endif

bool TraceRouteModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_RouteDiscovery *r)
{
    // We only alter the packet in alterReceivedProtobuf()
    return false; // let it be handled by RoutingModule
}

void TraceRouteModule::alterReceivedProtobuf(meshtastic_MeshPacket &p, meshtastic_RouteDiscovery *r)
{
    const meshtastic_Data &incoming = p.decoded;

    // Update next-hops using returned route
    if (incoming.request_id) {
        updateNextHops(p, r);
    }

    // Insert unknown hops if necessary
    insertUnknownHops(p, r, !incoming.request_id);

    // Append ID and SNR. If the last hop is to us, we only need to append the SNR
    appendMyIDandSNR(r, p.rx_snr, !incoming.request_id, isToUs(&p));
    if (!incoming.request_id)
        printRoute(r, p.from, p.to, true);
    else
        printRoute(r, p.to, p.from, false);

    // Set updated route 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_RouteDiscovery_msg, r);

    if (tracingNode != 0) {
        // check isResponseFromTarget
        bool isResponseFromTarget = (incoming.request_id != 0 && p.from == tracingNode);
        bool isRequestToUs = (incoming.request_id == 0 && p.to == nodeDB->getNodeNum() && tracingNode != 0);

        // Check if this is a trace route response containing our target node
        bool containsTargetNode = false;
        for (uint8_t i = 0; i < r->route_count; i++) {
            if (r->route[i] == tracingNode) {
                containsTargetNode = true;
                break;
            }
        }
        for (uint8_t i = 0; i < r->route_back_count; i++) {
            if (r->route_back[i] == tracingNode) {
                containsTargetNode = true;
                break;
            }
        }

        // Check if this response contains a complete route to our target
        bool hasCompleteRoute = (r->route_count > 0 && r->route_back_count > 0) ||
                                (containsTargetNode && (r->route_count > 0 || r->route_back_count > 0));

        LOG_INFO("TracRoute packet analysis: tracingNode=0x%08x, p.from=0x%08x, p.to=0x%08x, request_id=0x%08x", tracingNode,
                 p.from, p.to, incoming.request_id);
        LOG_INFO("TracRoute conditions: isResponseFromTarget=%d, isRequestToUs=%d, containsTargetNode=%d, hasCompleteRoute=%d",
                 isResponseFromTarget, isRequestToUs, containsTargetNode, hasCompleteRoute);

        if (isResponseFromTarget || isRequestToUs || (containsTargetNode && hasCompleteRoute)) {
            LOG_INFO("TracRoute result detected: isResponseFromTarget=%d, isRequestToUs=%d", isResponseFromTarget, isRequestToUs);

            LOG_INFO("SNR arrays - towards_count=%d, back_count=%d", r->snr_towards_count, r->snr_back_count);
            for (int i = 0; i < r->snr_towards_count; i++) {
                LOG_INFO("SNR towards[%d] = %d (%.1fdB)", i, r->snr_towards[i], (float)r->snr_towards[i] / 4.0f);
            }
            for (int i = 0; i < r->snr_back_count; i++) {
                LOG_INFO("SNR back[%d] = %d (%.1fdB)", i, r->snr_back[i], (float)r->snr_back[i] / 4.0f);
            }

            String result = "";

            // Show request path (from initiator to target)
            if (r->route_count > 0) {
                result += getNodeName(nodeDB->getNodeNum());
                for (uint8_t i = 0; i < r->route_count; i++) {
                    result += " > ";
                    const char *name = getNodeName(r->route[i]);
                    float snr =
                        (i < r->snr_towards_count && r->snr_towards[i] != INT8_MIN) ? ((float)r->snr_towards[i] / 4.0f) : 0.0f;
                    result += name;
                    if (snr != 0.0f) {
                        result += "(";
                        result += String(snr, 1);
                        result += "dB)";
                    }
                }
                result += " > ";
                result += getNodeName(tracingNode);
                if (r->snr_towards_count > 0 && r->snr_towards[r->snr_towards_count - 1] != INT8_MIN) {
                    result += "(";
                    result += String((float)r->snr_towards[r->snr_towards_count - 1] / 4.0f, 1);
                    result += "dB)";
                }
                result += "\n";
            } else {
                // Direct connection (no intermediate hops)
                result += getNodeName(nodeDB->getNodeNum());
                result += " > ";
                result += getNodeName(tracingNode);
                if (r->snr_towards_count > 0 && r->snr_towards[0] != INT8_MIN) {
                    result += "(";
                    result += String((float)r->snr_towards[0] / 4.0f, 1);
                    result += "dB)";
                }
                result += "\n";
            }

            // Show response path (from target back to initiator)
            if (r->route_back_count > 0) {
                result += getNodeName(tracingNode);
                for (int8_t i = r->route_back_count - 1; i >= 0; i--) {
                    result += " > ";
                    const char *name = getNodeName(r->route_back[i]);
                    float snr = (i < r->snr_back_count && r->snr_back[i] != INT8_MIN) ? ((float)r->snr_back[i] / 4.0f) : 0.0f;
                    result += name;
                    if (snr != 0.0f) {
                        result += "(";
                        result += String(snr, 1);
                        result += "dB)";
                    }
                }
                // add initiator node
                result += " > ";
                result += getNodeName(nodeDB->getNodeNum());
                if (r->snr_back_count > 0 && r->snr_back[r->snr_back_count - 1] != INT8_MIN) {
                    result += "(";
                    result += String((float)r->snr_back[r->snr_back_count - 1] / 4.0f, 1);
                    result += "dB)";
                }
            } else {
                // Direct return path (no intermediate hops)
                result += getNodeName(tracingNode);
                result += " > ";
                result += getNodeName(nodeDB->getNodeNum());
                if (r->snr_back_count > 0 && r->snr_back[0] != INT8_MIN) {
                    result += "(";
                    result += String((float)r->snr_back[0] / 4.0f, 1);
                    result += "dB)";
                }
            }

            LOG_INFO("Trace route result: %s", result.c_str());
            handleTraceRouteResult(result);
        }
    }
}

void TraceRouteModule::updateNextHops(const meshtastic_MeshPacket &p, meshtastic_RouteDiscovery *r)
{
    // E.g. if the route is A->B->C->D and we are B, we can set C as next-hop for C and D
    // Similarly, if we are C, we can set D as next-hop for D
    // If we are A, we can set B as next-hop for B, C and D

    // First check if we were the original sender or in the original route
    int8_t nextHopIndex = -1;
    if (isToUs(&p)) {
        nextHopIndex = 0; // We are the original sender, next hop is first in route
    } else {
        // Check if we are in the original route
        for (uint8_t i = 0; i < r->route_count; i++) {
            if (r->route[i] == nodeDB->getNodeNum()) {
                nextHopIndex = i + 1; // Next hop is the one after us
                break;
            }
        }
    }

    // If we are in the original route, update the next hops
    if (nextHopIndex != -1) {
        // For every node after us, we can set the next-hop to the first node after us
        NodeNum nextHop;
        if (nextHopIndex == r->route_count) {
            nextHop = p.from; // We are the last in the route, next hop is destination
        } else {
            nextHop = r->route[nextHopIndex];
        }

        if (nextHop == NODENUM_BROADCAST) {
            return;
        }
        uint8_t nextHopByte = nodeDB->getLastByteOfNodeNum(nextHop);

        // The route array is unauthenticated payload, so only learn from it when the node it names as our
        // next hop is the one that actually relayed this packet to us. Otherwise a forged response could
        // point any node's next_hop anywhere. relay_node is 0 for MQTT-sourced packets, which cannot
        // corroborate an RF route either.
        if (p.relay_node == NO_RELAY_NODE || nextHopByte != p.relay_node) {
            LOG_DEBUG("Ignore traceroute next-hop 0x%02x, relayed by 0x%02x", nextHopByte, p.relay_node);
            return;
        }

        // For the rest of the nodes in the route, set their next-hop
        // Note: if we are the last in the route, this loop will not run
        for (int8_t i = nextHopIndex; i < r->route_count; i++) {
            NodeNum targetNode = r->route[i];
            maybeSetNextHop(targetNode, nextHopByte);
        }

        // Also set next-hop for the destination node
        maybeSetNextHop(p.from, nextHopByte);
    }
}

void TraceRouteModule::maybeSetNextHop(NodeNum target, uint8_t nextHopByte)
{
    if (target == NODENUM_BROADCAST)
        return;

    meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(target);
    if (node && node->next_hop != nextHopByte) {
        LOG_INFO("Update next-hop for 0x%08x to 0x%02x via traceroute", target, nextHopByte);
        node->next_hop = nextHopByte;
    }

#if HAS_TRAFFIC_MANAGEMENT
    // Mirror into the TMM overflow cache. Traceroute is the highest-confidence
    // source (full known route), and this captures the target even when it isn't
    // in the hot NodeDB - same rationale as the ACK-confirmed path in NextHopRouter.
    if (trafficManagementModule)
        trafficManagementModule->setNextHop(target, nextHopByte);
#endif
}

void TraceRouteModule::processUpgradedPacket(const meshtastic_MeshPacket &mp)
{
    if (mp.which_payload_variant != meshtastic_MeshPacket_decoded_tag || mp.decoded.portnum != meshtastic_PortNum_TRACEROUTE_APP)
        return;

    meshtastic_RouteDiscovery decoded = meshtastic_RouteDiscovery_init_zero;
    if (!pb_decode_from_bytes(mp.decoded.payload.bytes, mp.decoded.payload.size, &meshtastic_RouteDiscovery_msg, &decoded))
        return;

    handleReceivedProtobuf(mp, &decoded);
    // Intentionally modify the packet in-place so downstream relays see our updates.
    alterReceivedProtobuf(const_cast<meshtastic_MeshPacket &>(mp), &decoded);
}

void TraceRouteModule::insertUnknownHops(meshtastic_MeshPacket &p, meshtastic_RouteDiscovery *r, bool isTowardsDestination)
{
    pb_size_t *route_count;
    uint32_t *route;
    pb_size_t *snr_count;
    int8_t *snr_list;

    // Pick the correct route array and SNR list
    if (isTowardsDestination) {
        route_count = &r->route_count;
        route = r->route;
        snr_count = &r->snr_towards_count;
        snr_list = r->snr_towards;
    } else {
        route_count = &r->route_back_count;
        route = r->route_back;
        snr_count = &r->snr_back_count;
        snr_list = r->snr_back;
    }

    // Only insert unknown hops if hop_start is valid
    const int8_t hopsTaken = getHopsAway(p);
    if (hopsTaken >= 0) {
        int8_t diff = hopsTaken - *route_count;
        for (int8_t i = 0; i < diff; i++) {
            if (*route_count < ROUTE_SIZE) {
                route[*route_count] = NODENUM_BROADCAST; // This will represent an unknown hop
                *route_count += 1;
            }
        }
        // Add unknown SNR values if necessary
        diff = *route_count - *snr_count;
        for (int8_t i = 0; i < diff; i++) {
            if (*snr_count < ROUTE_SIZE) {
                snr_list[*snr_count] = INT8_MIN; // This will represent an unknown SNR
                *snr_count += 1;
            }
        }
    }
}

void TraceRouteModule::appendMyIDandSNR(meshtastic_RouteDiscovery *updated, float snr, bool isTowardsDestination, bool SNRonly)
{
    pb_size_t *route_count;
    uint32_t *route;
    pb_size_t *snr_count;
    int8_t *snr_list;

    // Pick the correct route array and SNR list
    if (isTowardsDestination) {
        route_count = &updated->route_count;
        route = updated->route;
        snr_count = &updated->snr_towards_count;
        snr_list = updated->snr_towards;
    } else {
        route_count = &updated->route_back_count;
        route = updated->route_back;
        snr_count = &updated->snr_back_count;
        snr_list = updated->snr_back;
    }

    if (*snr_count < ROUTE_SIZE) {
        // Clamp before the cast: q4-scaled SNR at or below the demodulation floor can reach
        // -128 (=-32dB), which is bit-identical to the INT8_MIN "unknown SNR" sentinel used
        // throughout this file. Reserve -128 for the sentinel; clamp real readings to -127.
        int32_t q4 = clamp<int32_t>(lroundf(snr * 4.0f), -127, 127);
        snr_list[*snr_count] = (int8_t)q4;
        *snr_count += 1;
    }
    if (SNRonly)
        return;

    // Length of route array can normally not be exceeded due to the max. hop_limit of 7
    if (*route_count < ROUTE_SIZE) {
        route[*route_count] = myNodeInfo.my_node_num;
        *route_count += 1;
    } else {
        LOG_WARN("Route exceeded max hop limit"); // Are you bridging networks?
    }
}

void TraceRouteModule::printRoute(meshtastic_RouteDiscovery *r, uint32_t origin, uint32_t dest, bool isTowardsDestination)
{
#if defined(DEBUG_PORT) && !defined(DEBUG_MUTE)
    std::string route = "Route traced:\n";
    route += vformat("0x%x --> ", origin);
    for (uint8_t i = 0; i < r->route_count; i++) {
        if (i < r->snr_towards_count && r->snr_towards[i] != INT8_MIN)
            route += vformat("0x%x (%.2fdB) --> ", r->route[i], (float)r->snr_towards[i] / 4);
        else
            route += vformat("0x%x (?dB) --> ", r->route[i]);
    }
    // If we are the destination, or it has already reached the destination, print it
    if (dest == nodeDB->getNodeNum() || !isTowardsDestination) {
        if (r->snr_towards_count > 0 && r->snr_towards[r->snr_towards_count - 1] != INT8_MIN)
            route += vformat("0x%x (%.2fdB)", dest, (float)r->snr_towards[r->snr_towards_count - 1] / 4);

        else
            route += vformat("0x%x (?dB)", dest);
    } else
        route += "...";

    // If there's a route back (or we are the destination as then the route is complete), print it
    if (r->route_back_count > 0 || origin == nodeDB->getNodeNum()) {
        route += "\n";
        if (origin == nodeDB->getNodeNum() && r->snr_back_count > 0 && r->snr_back[r->snr_back_count - 1] != INT8_MIN)
            route += vformat("(%.2fdB) 0x%x <-- ", (float)r->snr_back[r->snr_back_count - 1] / 4, origin);
        else
            route += "...";

        for (int8_t i = r->route_back_count - 1; i >= 0; i--) {
            if (i < r->snr_back_count && r->snr_back[i] != INT8_MIN)
                route += vformat("(%.2fdB) 0x%x <-- ", (float)r->snr_back[i] / 4, r->route_back[i]);
            else
                route += vformat("(?dB) 0x%x <-- ", r->route_back[i]);
        }
        route += vformat("0x%x", dest);
    }
    LOG_INFO(route.c_str());
#endif
}

meshtastic_MeshPacket *TraceRouteModule::allocReply()
{
    assert(currentRequest);

    // Ignore multi-hop broadcast requests
    if (isBroadcast(currentRequest->to) && currentRequest->hop_limit < currentRequest->hop_start) {
        ignoreRequest = true;
        return NULL;
    }

    // Copy the payload of the current request
    auto req = *currentRequest;
    const auto &p = req.decoded;
    meshtastic_RouteDiscovery scratch;
    meshtastic_RouteDiscovery *updated = NULL;
    memset(&scratch, 0, sizeof(scratch));
    pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_RouteDiscovery_msg, &scratch);
    updated = &scratch;

    // Create a MeshPacket with this payload and set it as the reply
    meshtastic_MeshPacket *reply = allocDataProtobuf(*updated);

    return reply;
}

TraceRouteModule::TraceRouteModule()
    : ProtobufModule("traceroute", meshtastic_PortNum_TRACEROUTE_APP, &meshtastic_RouteDiscovery_msg), OSThread("TraceRoute")
{
    ourPortNum = meshtastic_PortNum_TRACEROUTE_APP;
    isPromiscuous = true; // We need to update the route even if it is not destined to us
}

const char *TraceRouteModule::getNodeName(NodeNum node)
{
    meshtastic_NodeInfoLite *info = nodeDB->getMeshNode(node);
    if (nodeInfoLiteHasUser(info)) {
        if (strlen(info->short_name) > 0) {
            return info->short_name;
        }
        if (strlen(info->long_name) > 0) {
            return info->long_name;
        }
    }

    static char fallback[12];
    snprintf(fallback, sizeof(fallback), "0x%08x", node);
    return fallback;
}

bool TraceRouteModule::startTraceRoute(NodeNum node)
{
    LOG_INFO("TraceRoute startTraceRoute: node=0x%08x", node);
    unsigned long now = millis();

    if (node == 0 || node == NODENUM_BROADCAST) {
        LOG_ERROR("Invalid trace route node: 0x%08x", node);
        runState = TRACEROUTE_STATE_RESULT;
        setResultText("Invalid node");
        resultShowTime = millis();
        tracingNode = 0;

        requestFocus();
        UIFrameEvent e;
        e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
        notifyObservers(&e);
        return false;
    }

    if (node == nodeDB->getNodeNum()) {
        LOG_ERROR("Can't trace route to self: 0x%08x", node);
        runState = TRACEROUTE_STATE_RESULT;
        setResultText("Cannot trace self");
        resultShowTime = millis();
        tracingNode = 0;

        requestFocus();
        UIFrameEvent e;
        e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
        notifyObservers(&e);
        return false;
    }

    if (!initialized) {
        lastTraceRouteTime = 0;
        initialized = true;
        LOG_INFO("TraceRoute first init");
    }

    if (runState == TRACEROUTE_STATE_TRACKING) {
        LOG_INFO("TraceRoute already in progress");
        return false;
    }

    if (initialized && lastTraceRouteTime > 0 && now - lastTraceRouteTime < cooldownMs) {
        // Cooldown
        unsigned long wait = (cooldownMs - (now - lastTraceRouteTime)) / 1000;
        bannerText = String("Wait for ") + String(wait) + String("s");
        runState = TRACEROUTE_STATE_COOLDOWN;
        resultText = "";
        clearResultLines();

        requestFocus();
        UIFrameEvent e;
        e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
        notifyObservers(&e);
        LOG_INFO("Cooldown active, wait %lu sec before new trace route", wait);
        return false;
    }

    tracingNode = node;
    lastTraceRouteTime = now;
    runState = TRACEROUTE_STATE_TRACKING;
    resultText = "";
    clearResultLines();
    bannerText = String("Tracing ") + getNodeName(node);

    LOG_INFO("TraceRoute UI: Start trace to 0x%08x, request focus", node);

    // 请求焦点，然后触发UI更新事件
    requestFocus();
    UIFrameEvent e;
    e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
    notifyObservers(&e);

    // 设置定时器来处理超时检查
    setIntervalFromNow(1000); // 每秒检查一次状态

    meshtastic_RouteDiscovery req = meshtastic_RouteDiscovery_init_zero;
    LOG_INFO("Creating RouteDiscovery protobuf");

    // Allocate a packet directly from router like the reference code
    meshtastic_MeshPacket *p = router->allocForSending();
    if (p) {
        // Set destination and port
        p->to = node;
        p->decoded.portnum = meshtastic_PortNum_TRACEROUTE_APP;
        p->decoded.want_response = true;

        // Use reliable delivery for traceroute requests (which will be copied to traceroute responses by setReplyTo)
        p->want_ack = true;

        // Manually encode the RouteDiscovery payload
        p->decoded.payload.size =
            pb_encode_to_bytes(p->decoded.payload.bytes, sizeof(p->decoded.payload.bytes), &meshtastic_RouteDiscovery_msg, &req);

        LOG_INFO("Packet allocated: to=0x%08x, portnum=%d, want_response=%d, payload_size=%d", p->to, p->decoded.portnum,
                 p->decoded.want_response, p->decoded.payload.size);
        LOG_INFO("Calling service->sendToMesh");

        if (service) {
            LOG_INFO("MeshService is available, sending packet");
            service->sendToMesh(p, RX_SRC_USER);
            LOG_INFO("sendToMesh called for trace route to node 0x%08x", node);
        } else {
            LOG_ERROR("MeshService is NULL");
            runState = TRACEROUTE_STATE_RESULT;
            setResultText("Service unavailable");
            resultShowTime = millis();
            tracingNode = 0;

            requestFocus();
            UIFrameEvent e2;
            e2.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
            notifyObservers(&e2);
            return false;
        }
    } else {
        LOG_ERROR("TraceRoute packet alloc from router failed");
        runState = TRACEROUTE_STATE_RESULT;
        setResultText("Failed to send");
        resultShowTime = millis();
        tracingNode = 0;

        requestFocus();
        UIFrameEvent e2;
        e2.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
        notifyObservers(&e2);
        return false;
    }
    return true;
}

void TraceRouteModule::launch(NodeNum node)
{
    if (node == 0 || node == NODENUM_BROADCAST) {
        LOG_ERROR("Invalid trace route node: 0x%08x", node);
        runState = TRACEROUTE_STATE_RESULT;
        setResultText("Invalid node");
        resultShowTime = millis();
        tracingNode = 0;

        requestFocus();
        UIFrameEvent e;
        e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
        notifyObservers(&e);
        return;
    }

    if (node == nodeDB->getNodeNum()) {
        LOG_ERROR("Can't trace route to self: 0x%08x", node);
        runState = TRACEROUTE_STATE_RESULT;
        setResultText("Cannot trace self");
        resultShowTime = millis();
        tracingNode = 0;

        requestFocus();
        UIFrameEvent e;
        e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
        notifyObservers(&e);
        return;
    }

    if (!initialized) {
        lastTraceRouteTime = 0;
        initialized = true;
        LOG_INFO("TraceRoute first init");
    }

    unsigned long now = millis();
    if (initialized && lastTraceRouteTime > 0 && now - lastTraceRouteTime < cooldownMs) {
        unsigned long wait = (cooldownMs - (now - lastTraceRouteTime)) / 1000;
        bannerText = String("Wait for ") + String(wait) + String("s");
        runState = TRACEROUTE_STATE_COOLDOWN;
        resultText = "";
        clearResultLines();

        requestFocus();
        UIFrameEvent e;
        e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
        notifyObservers(&e);
        LOG_INFO("Cooldown active, wait %lu sec before new trace route", wait);
        return;
    }

    runState = TRACEROUTE_STATE_TRACKING;
    tracingNode = node;
    lastTraceRouteTime = now;
    resultText = "";
    clearResultLines();
    bannerText = String("Tracing ") + getNodeName(node);

    requestFocus();

    UIFrameEvent e;
    e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
    notifyObservers(&e);

    setIntervalFromNow(1000);

    meshtastic_RouteDiscovery req = meshtastic_RouteDiscovery_init_zero;
    LOG_INFO("Creating RouteDiscovery protobuf");

    meshtastic_MeshPacket *p = router->allocForSending();
    if (p) {
        p->to = node;
        p->decoded.portnum = meshtastic_PortNum_TRACEROUTE_APP;
        p->decoded.want_response = true;

        // Use reliable delivery for traceroute requests (which will be copied to traceroute responses by setReplyTo)
        p->want_ack = true;

        p->decoded.payload.size =
            pb_encode_to_bytes(p->decoded.payload.bytes, sizeof(p->decoded.payload.bytes), &meshtastic_RouteDiscovery_msg, &req);

        LOG_INFO("Packet allocated: to=0x%08x, portnum=%d, want_response=%d, payload_size=%d", p->to, p->decoded.portnum,
                 p->decoded.want_response, p->decoded.payload.size);

        if (service) {
            service->sendToMesh(p, RX_SRC_USER);
            LOG_INFO("sendToMesh called for trace route to node 0x%08x", node);
        } else {
            LOG_ERROR("MeshService is NULL");
            runState = TRACEROUTE_STATE_RESULT;
            setResultText("Service unavailable");
            resultShowTime = millis();
            tracingNode = 0;
        }
    } else {
        LOG_ERROR("TraceRoute packet alloc from router failed");
        runState = TRACEROUTE_STATE_RESULT;
        setResultText("Failed to send");
        resultShowTime = millis();
        tracingNode = 0;
    }
}

void TraceRouteModule::handleTraceRouteResult(const String &result)
{
    setResultText(result);
    runState = TRACEROUTE_STATE_RESULT;
    resultShowTime = millis();
    tracingNode = 0;

    LOG_INFO("TraceRoute result ready, request focus: %s", result.c_str());

    setIntervalFromNow(1000);

    requestFocus();
    UIFrameEvent e;
    e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
    notifyObservers(&e);

    LOG_INFO("TraceRoute handleTraceRouteResult END");
}

bool TraceRouteModule::shouldDraw()
{
    bool draw = (runState != TRACEROUTE_STATE_IDLE);
    static TraceRouteRunState lastLoggedState = TRACEROUTE_STATE_IDLE;
    if (runState != lastLoggedState) {
        LOG_INFO("TraceRoute shouldDraw: runState=%d, draw=%d", runState, draw);
        lastLoggedState = runState;
    }
    return draw;
}
#if HAS_SCREEN
void TraceRouteModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
{
    LOG_DEBUG("TraceRoute drawFrame called: runState=%d", runState);

    display->setTextAlignment(TEXT_ALIGN_CENTER);

    if (runState == TRACEROUTE_STATE_TRACKING) {
        display->setFont(FONT_MEDIUM);
        int centerY = y + (display->getHeight() / 2) - (FONT_HEIGHT_MEDIUM / 2);
        display->drawString(display->getWidth() / 2 + x, centerY, bannerText);

    } else if (runState == TRACEROUTE_STATE_RESULT) {
        display->setFont(FONT_MEDIUM);
        display->setTextAlignment(TEXT_ALIGN_LEFT);

        display->drawString(x, y, "Route Result");

        int contentStartY = y + FONT_HEIGHT_MEDIUM + 2; // Add more spacing after title
        display->setTextAlignment(TEXT_ALIGN_LEFT);
        display->setFont(FONT_SMALL);

        if (resultText.length() > 0) {
            if (resultLinesDirty) {
                rebuildResultLines(display);
            }

            int lineHeight = FONT_HEIGHT_SMALL + 1; // Use proper font height with 1px spacing
            for (size_t i = 0; i < resultLines.size(); i++) {
                int lineY = contentStartY + (i * lineHeight);
                if (lineY + FONT_HEIGHT_SMALL <= display->getHeight()) {
                    display->drawString(x + 2, lineY, resultLines[i]);
                }
            }
        }

    } else if (runState == TRACEROUTE_STATE_COOLDOWN) {
        display->setFont(FONT_MEDIUM);
        int centerY = y + (display->getHeight() / 2) - (FONT_HEIGHT_MEDIUM / 2);
        display->drawString(display->getWidth() / 2 + x, centerY, bannerText);
    }
}
#endif // HAS_SCREEN
int32_t TraceRouteModule::runOnce()
{
    unsigned long now = millis();

    if (runState == TRACEROUTE_STATE_IDLE) {
        return INT32_MAX;
    }

    // Check for tracking timeout
    if (runState == TRACEROUTE_STATE_TRACKING && now - lastTraceRouteTime > trackingTimeoutMs) {
        LOG_INFO("TraceRoute timeout, no response");
        runState = TRACEROUTE_STATE_RESULT;
        setResultText("No response received");
        resultShowTime = now;
        tracingNode = 0;

        requestFocus();
        UIFrameEvent e;
        e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
        notifyObservers(&e);

        setIntervalFromNow(resultDisplayMs);
        return resultDisplayMs;
    }

    // Update cooldown display every second
    if (runState == TRACEROUTE_STATE_COOLDOWN) {
        unsigned long wait = (cooldownMs - (now - lastTraceRouteTime)) / 1000;
        if (wait > 0) {
            String newBannerText = String("Wait for ") + String(wait) + String("s");
            bannerText = newBannerText;
            LOG_INFO("TraceRoute cooldown: updating banner to %s", bannerText.c_str());

            // Force flash UI
            requestFocus();
            UIFrameEvent e;
            e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
            notifyObservers(&e);

            if (screen) {
                screen->forceDisplay();
            }

            return 1000;
        } else {
            // Cooldown finished
            LOG_INFO("TraceRoute cooldown done, return to IDLE");
            runState = TRACEROUTE_STATE_IDLE;
            resultText = "";
            clearResultLines();
            bannerText = "";
            UIFrameEvent e;
            e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
            notifyObservers(&e);
            return INT32_MAX;
        }
    }

    if (runState == TRACEROUTE_STATE_RESULT) {
        if (now - resultShowTime >= resultDisplayMs) {
            LOG_INFO("TraceRoute result display timeout, returning to IDLE");
            runState = TRACEROUTE_STATE_IDLE;
            resultText = "";
            clearResultLines();
            bannerText = "";
            tracingNode = 0;
            UIFrameEvent e;
            e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
            notifyObservers(&e);
            return INT32_MAX;
        } else {
            return 1000;
        }
    }

    if (runState == TRACEROUTE_STATE_TRACKING) {
        return 1000;
    }

    return INT32_MAX;
}
