#include "configuration.h"
#if HAS_SCREEN
#include "ClockRenderer.h"
#include "Default.h"
#include "DisplayFormatters.h"
#include "GPS.h"
#include "MenuHandler.h"
#include "MeshRadio.h"
#include "MeshService.h"
#include "MessageStore.h"
#include "NodeDB.h"
#include "buzz.h"
#include "graphics/Screen.h"
#include "graphics/SharedUIDisplay.h"
#include "graphics/TFTColorRegions.h"
#include "graphics/l10n/Strings.h"
#include "graphics/draw/MessageRenderer.h"
#include "graphics/draw/UIRenderer.h"
#include "input/RotaryEncoderInterruptImpl1.h"
#include "input/UpDownInterruptImpl1.h"
#include "main.h"
#include "mesh/Default.h"
#include "mesh/MeshTypes.h"
#include "mesh/RadioLibInterface.h"
#include "modules/AdminModule.h"
#include "modules/CannedMessageModule.h"
#include "modules/ExternalNotificationModule.h"
#include "modules/KeyVerificationModule.h"
#include "modules/TraceRouteModule.h"
#include <algorithm>
#include <array>
#include <cmath>
#include <functional>
#include <utility>

namespace graphics
{

namespace
{

// Caller must ensure the provided options array outlives the banner callback.
template <typename T, size_t N, typename Callback>
BannerOverlayOptions createStaticBannerOptions(const char *message, const MenuOption<T> (&options)[N],
                                               std::array<const char *, N> &labels, Callback &&onSelection)
{
    for (size_t i = 0; i < N; ++i) {
        labels[i] = options[i].label;
    }

    const MenuOption<T> *optionsPtr = options;
    auto callback = std::function<void(const MenuOption<T> &, int)>(std::forward<Callback>(onSelection));

    BannerOverlayOptions bannerOptions;
    bannerOptions.message = message;
    bannerOptions.optionsArrayPtr = labels.data();
    bannerOptions.optionsCount = static_cast<uint8_t>(N);
    bannerOptions.bannerCallback = [optionsPtr, callback](int selected) -> void { callback(optionsPtr[selected], selected); };
    return bannerOptions;
}

const StoredMessage *getNewestMessageForActiveThread()
{
    const auto &messages = messageStore.getMessages();
    if (messages.empty()) {
        return nullptr;
    }

    const auto mode = graphics::MessageRenderer::getThreadMode();
    const int channel = graphics::MessageRenderer::getThreadChannel();
    const uint32_t peer = graphics::MessageRenderer::getThreadPeer();
    const uint32_t localNode = nodeDB->getNodeNum();

    if (mode == graphics::MessageRenderer::ThreadMode::ALL) {
        return &messages.back();
    }

    for (auto it = messages.rbegin(); it != messages.rend(); ++it) {
        const StoredMessage &m = *it;

        if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) {
            if (m.type == MessageType::BROADCAST && static_cast<int>(m.channelIndex) == channel) {
                return &m;
            }
            continue;
        }

        if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) {
            if (m.type != MessageType::DM_TO_US) {
                continue;
            }
            const uint32_t other = (m.sender == localNode) ? m.dest : m.sender;
            if (other == peer) {
                return &m;
            }
        }
    }

    return nullptr;
}

void launchReplyForMessage(const StoredMessage &message, bool freetext)
{
    if (message.type == MessageType::BROADCAST || message.dest == NODENUM_BROADCAST) {
        if (freetext) {
            cannedMessageModule->LaunchFreetextWithDestination(NODENUM_BROADCAST, message.channelIndex);
        } else {
            cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST, message.channelIndex);
        }
        return;
    }

    const uint32_t localNode = nodeDB->getNodeNum();
    const uint32_t peer = (message.sender == localNode) ? message.dest : message.sender;
    if (peer == 0 || peer == NODENUM_BROADCAST) {
        return;
    }

    if (freetext) {
        cannedMessageModule->LaunchFreetextWithDestination(peer);
    } else {
        cannedMessageModule->LaunchWithDestination(peer);
    }
}

} // namespace

menuHandler::screenMenus menuHandler::menuQueue = MenuNone;
uint32_t menuHandler::pickedNodeNum = 0;
meshtastic_Config_LoRaConfig_RegionCode menuHandler::pendingRegion = meshtastic_Config_LoRaConfig_RegionCode_UNSET;
bool test_enabled = false;
uint8_t test_count = 0;

void menuHandler::loraMenu()
{
    static const char *optionsArray[] = {"Back", "Device Role", "Radio Preset", "Frequency Slot", "LoRa Region"};
    enum optionsNumbers { Back = 0, DeviceRolePicker = 1, RadioPresetPicker = 2, FrequencySlot = 3, LoraPicker = 4 };
    BannerOverlayOptions bannerOptions;
    bannerOptions.message = "LoRa Actions";
    bannerOptions.optionsArrayPtr = optionsArray;
    bannerOptions.optionsCount = 5;
    bannerOptions.bannerCallback = [](int selected) -> void {
        if (selected == Back) {
            // No action
        } else if (selected == DeviceRolePicker) {
            menuHandler::menuQueue = menuHandler::DeviceRolePicker;
        } else if (selected == RadioPresetPicker) {
            menuHandler::menuQueue = menuHandler::RadioPresetPicker;
        } else if (selected == FrequencySlot) {
            menuHandler::menuQueue = menuHandler::FrequencySlot;
        } else if (selected == LoraPicker) {
            menuHandler::menuQueue = menuHandler::LoraPicker;
        }
    };
    screen->showOverlayBanner(bannerOptions);
}

void menuHandler::OnboardMessage()
{
    static const char *optionsArray[] = {"OK", "Got it!"};
    enum optionsNumbers { OK, got };
    BannerOverlayOptions bannerOptions;
#if HAS_TFT
    bannerOptions.message = "Welcome to Meshtastic!\nSwipe to navigate and\nlong press to select\nor open a menu.";
#elif defined(BUTTON_PIN)
    bannerOptions.message = "Welcome to Meshtastic!\nClick to navigate and\nlong press to select\nor open a menu.";
#else
    bannerOptions.message = "Welcome to Meshtastic!\nUse the Select button\nto open menus\nand make selections.";
#endif
    bannerOptions.optionsArrayPtr = optionsArray;
    bannerOptions.optionsCount = 2;
    bannerOptions.bannerCallback = [](int selected) -> void {
        menuHandler::menuQueue = menuHandler::NoTimeoutLoraPicker;
        screen->runNow();
    };
    screen->showOverlayBanner(bannerOptions);
}

static void applyLoraRegion(meshtastic_Config_LoRaConfig_RegionCode region, bool isHam)
{
    config.lora.region = region;
    config.lora.channel_num = 0; // Reset to default channel

    // Reconcile the preset with the explicitly chosen region: a preset locked to another
    // region would leave config.lora invalid until applyModemConfig() repairs it with
    // error/critical-error side effects — or, for the swappable EU trio, the clamp would
    // flip the region right back. The user picked the region, so the preset follows it.
    const RegionInfo *newRegion = getRegion(region);
    if (config.lora.use_preset && !newRegion->supportsPreset(config.lora.modem_preset)) {
        LOG_INFO("Preset %s not available in %s, using default %s",
                 DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false, true), newRegion->name,
                 DisplayFormatters::getModemPresetDisplayName(newRegion->getDefaultPreset(), false, true));
        config.lora.modem_preset = newRegion->getDefaultPreset();
    }

    if (isHam && adminModule) {
        meshtastic_HamParameters hamParams = meshtastic_HamParameters_init_zero;
        strncpy(hamParams.call_sign, "N0CALL", sizeof(hamParams.call_sign) - 1);
        strncpy(hamParams.short_name, "N0CL", sizeof(hamParams.short_name));
        hamParams.tx_power = config.lora.tx_power;
        hamParams.frequency = config.lora.override_frequency;
        adminModule->handleSetHamMode(hamParams);
    }
    auto changes = SEGMENT_CONFIG;
#if !(MESHTASTIC_EXCLUDE_PKI_KEYGEN || MESHTASTIC_EXCLUDE_PKI)
    if (crypto) {
        crypto->ensurePkiKeys(config.security, owner);
    }
#endif
    initRegion();
    if (getEffectiveDutyCycle() < 100) {
        config.lora.ignore_mqtt = true;
    }
    if (strncmp(moduleConfig.mqtt.root, default_mqtt_root, strlen(default_mqtt_root)) == 0) {
        snprintf(moduleConfig.mqtt.root, sizeof(moduleConfig.mqtt.root), "%s/%s", default_mqtt_root, myRegion->name);
        changes |= SEGMENT_MODULECONFIG;
    }
    service->reloadConfig(changes);
}

void menuHandler::LoraRegionPicker(uint32_t duration)
{
    static const LoraRegionOption regionOptions[] = {
        {"Back", OptionsAction::Back},
        {"US", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_US},
        {"EU_433", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_EU_433},
        {"EU_868", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_EU_868},
        {"EU_866", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_EU_866},
        {"EU_868_NARROW", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_EU_N_868},
        {"CN", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_CN},
        {"JP", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_JP},
        {"ANZ", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_ANZ},
        {"KR", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_KR},
        {"TW", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_TW},
        {"RU", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_RU},
        {"IN", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_IN},
        {"NZ_865", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_NZ_865},
        {"TH", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_TH},
        {"LORA_24", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_LORA_24},
        {"UA_433", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_UA_433},
        {"UA_868", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_UA_868},
        {"MY_433", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_MY_433},
        {"MY_919", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_MY_919},
        {"SG_923", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_SG_923},
        {"PH_433", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_PH_433},
        {"PH_868", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_PH_868},
        {"PH_915", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_PH_915},
        {"ANZ_433", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_ANZ_433},
        {"KZ_433", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_KZ_433},
        {"KZ_863", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_KZ_863},
        {"NP_865", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_NP_865},
        {"BR_902", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_BR_902},
        {"ITU1_2M (144-146)", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_ITU1_2M},
        {"ITU2_2M (144-148)", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_ITU2_2M},
        {"ITU3_2M (144-148)", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_ITU3_2M},
        {"ITU2_125CM (220-225)", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_ITU2_125CM},

    };

    constexpr size_t regionCount = sizeof(regionOptions) / sizeof(regionOptions[0]);
    static std::array<const char *, regionCount> regionLabels{};

    const char *bannerMessage = "Set the LoRa region";
    if (currentResolution == ScreenResolution::UltraLow) {
        bannerMessage = "LoRa Region";
    }

    auto bannerOptions =
        createStaticBannerOptions(bannerMessage, regionOptions, regionLabels, [](const LoraRegionOption &option, int) -> void {
            if (!option.hasValue) {
                return;
            }

            auto selectedRegion = option.value;
            if (config.lora.region == selectedRegion) {
                return;
            }

            // Guard: without a reboot, reconfigure() applies the region directly, so reject
            // regions this node can't use up front: unrecognized codes, licensed-only regions,
            // and radio hardware mismatches (2.4 GHz vs sub-GHz) — the same checks the admin
            // set-config path applies, but side-effect-free: ignoring a menu selection should
            // not record a critical error or notify clients. getRadio() used to catch hardware
            // mismatches post-reboot only.
            auto candidateLora = config.lora;
            candidateLora.region = selectedRegion;
            char regionErr[160];
            if (!RadioInterface::checkConfigRegion(candidateLora, regionErr, sizeof(regionErr))) {
                LOG_WARN("Ignoring region selection: %s", regionErr);
                return;
            }

            bool hamMode = getRegion(selectedRegion)->profile->licensedOnly;
            if (hamMode) {
                LOG_INFO("User chose an amateur radio mode region");
                pendingRegion = selectedRegion;
                menuQueue = HamModeConfirm;
                screen->runNow();
            } else if (owner.is_licensed) {
                LOG_INFO("Licensed user chose a non-ham region; prompting to revert licensed mode");
                pendingRegion = selectedRegion;
                menuQueue = LicensedToNormalConfirm;
                screen->runNow();
            } else {
                applyLoraRegion(selectedRegion, false);
            }
        });

    bannerOptions.durationMs = duration;

    int initialSelection = 0;
    for (size_t i = 0; i < regionCount; ++i) {
        if (regionOptions[i].hasValue && regionOptions[i].value == config.lora.region) {
            initialSelection = static_cast<int>(i);
            break;
        }
    }
    bannerOptions.InitialSelected = initialSelection;

    screen->showOverlayBanner(bannerOptions);
}

void menuHandler::hamModeConfirmMenu()
{
    static const char *confirmOptions[] = {"No", "Yes"};
    BannerOverlayOptions confirmBanner;
    confirmBanner.message = "I confirm I am a\nlicensed amateur\nradio operator";
    confirmBanner.optionsArrayPtr = confirmOptions;
    confirmBanner.optionsCount = 2;
    confirmBanner.bannerCallback = [](int selected) {
        if (selected == 1)
            applyLoraRegion(pendingRegion, true);
    };
    screen->showOverlayBanner(confirmBanner);
}

void menuHandler::licensedToNormalConfirmMenu()
{
    static const char *confirmOptions[] = {"Keep licensed", "Revert to Normal"};
    BannerOverlayOptions confirmBanner;
    confirmBanner.message = "Revert licensed\nmode? This will\nre-enable encryption.";
    confirmBanner.optionsArrayPtr = confirmOptions;
    confirmBanner.optionsCount = 2;
    confirmBanner.bannerCallback = [](int selected) {
        if (selected == 1) {
            owner.is_licensed = false;
            config.lora.override_duty_cycle = false;
            service->reloadOwner(false);
        }
        applyLoraRegion(pendingRegion, false);
    };
    screen->showOverlayBanner(confirmBanner);
}

void menuHandler::deviceRolePicker()
{
    static const char *optionsArray[] = {"Back", "Client", "Client Mute", "Lost and Found", "Tracker"};
    enum optionsNumbers {
        Back = 0,
        devicerole_client = 1,
        devicerole_clientmute = 2,
        devicerole_lostandfound = 3,
        devicerole_tracker = 4
    };
    BannerOverlayOptions bannerOptions;
    bannerOptions.message = "Device Role";
    bannerOptions.optionsArrayPtr = optionsArray;
    bannerOptions.optionsCount = 5;
    bannerOptions.bannerCallback = [](int selected) -> void {
        if (selected == Back) {
            menuHandler::menuQueue = menuHandler::LoraMenu;
            screen->runNow();
            return;
        } else if (selected == devicerole_client) {
            config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT;
        } else if (selected == devicerole_clientmute) {
            config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT_MUTE;
        } else if (selected == devicerole_lostandfound) {
            config.device.role = meshtastic_Config_DeviceConfig_Role_LOST_AND_FOUND;
        } else if (selected == devicerole_tracker) {
            config.device.role = meshtastic_Config_DeviceConfig_Role_TRACKER;
        }
        service->reloadConfig(SEGMENT_CONFIG);
        rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000);
    };
    screen->showOverlayBanner(bannerOptions);
}

void menuHandler::FrequencySlotPicker()
{

    enum ReplyOptions : int { Back = -1 };
    constexpr int MAX_CHANNEL_OPTIONS = 202;
    static const char *optionsArray[MAX_CHANNEL_OPTIONS];
    static int optionsEnumArray[MAX_CHANNEL_OPTIONS];
    static char channelText[MAX_CHANNEL_OPTIONS - 1][12];
    int options = 0;
    optionsArray[options] = "Back";
    optionsEnumArray[options++] = Back;
    optionsArray[options] = "Slot 0 (Auto)";
    optionsEnumArray[options++] = 0;

    // Calculate number of channels (copied from RadioInterface::applyModemConfig())

    meshtastic_Config_LoRaConfig &loraConfig = config.lora;
    double bw = loraConfig.use_preset ? modemPresetToBwKHz(loraConfig.modem_preset, myRegion->wideLora)
                                      : bwCodeToKHz(loraConfig.bandwidth);

    uint32_t numChannels = 0;
    if (myRegion) {
        // Match RadioInterface::applyModemConfig(): include padding, add spacing in numerator, and use round()
        const double spacing = myRegion->profile->spacing;
        const double padding = myRegion->profile->padding;
        const double channelBandwidthMHz = bw / 1000.0;
        const double numerator = (myRegion->freqEnd - myRegion->freqStart) + spacing;
        const double denominator = spacing + (padding * 2) + channelBandwidthMHz;
        if (denominator > 0.0) {
            numChannels = static_cast<uint32_t>(round(numerator / denominator));
        } else {
            LOG_WARN("Invalid region configuration: non-positive channel spacing/width");
        }
    } else {
        LOG_WARN("Region not set, cannot calculate number of channels");
        return;
    }

    if (numChannels > (uint32_t)(MAX_CHANNEL_OPTIONS - 2))
        numChannels = (uint32_t)(MAX_CHANNEL_OPTIONS - 2);

    for (uint32_t ch = 1; ch <= numChannels; ch++) {
        snprintf(channelText[ch - 1], sizeof(channelText[ch - 1]), "Slot %lu", (unsigned long)ch);
        optionsArray[options] = channelText[ch - 1];
        optionsEnumArray[options++] = (int)ch;
    }

    BannerOverlayOptions bannerOptions;
    bannerOptions.message = "Frequency Slot";
    bannerOptions.optionsArrayPtr = optionsArray;
    bannerOptions.optionsEnumPtr = optionsEnumArray;
    bannerOptions.optionsCount = options;

    // Start highlight on current channel if possible, otherwise on "1"
    int initial = (int)config.lora.channel_num + 1;
    if (initial < 2 || initial > (int)numChannels + 1)
        initial = 1;
    bannerOptions.InitialSelected = initial;

    bannerOptions.bannerCallback = [](int selected) -> void {
        if (selected == Back) {
            menuHandler::menuQueue = menuHandler::LoraMenu;
            screen->runNow();
            return;
        }

        config.lora.channel_num = selected;
        service->reloadConfig(SEGMENT_CONFIG);
    };

    screen->showOverlayBanner(bannerOptions);
}

// Maximum presets any region can have + 1 for Back
static constexpr int MAX_PRESET_OPTIONS = 16;

static BannerOverlayOptions buildRegionPresetBanner()
{
    // Static storage reused each call — safe because the banner is shown immediately after.
    static const char *optionsArray[MAX_PRESET_OPTIONS];
    static int optionsEnumArray[MAX_PRESET_OPTIONS];
    static char presetLabelBuf[MAX_PRESET_OPTIONS][12]; // scratch space for name copies
    int count = 0;

    optionsArray[count] = "Back";
    optionsEnumArray[count++] = -1;

    if (myRegion && myRegion->profile) {
        const meshtastic_Config_LoRaConfig_ModemPreset *presets = myRegion->getAvailablePresets();
        size_t numPresets = myRegion->getNumPresets();
        for (size_t i = 0; i < numPresets && count < MAX_PRESET_OPTIONS; ++i) {
            const char *name = DisplayFormatters::getModemPresetDisplayName(presets[i], false, true);
            strncpy(presetLabelBuf[count], name, sizeof(presetLabelBuf[count]) - 1);
            presetLabelBuf[count][sizeof(presetLabelBuf[count]) - 1] = '\0';
            optionsArray[count] = presetLabelBuf[count];
            optionsEnumArray[count++] = static_cast<int>(presets[i]);
        }
    }

    int initialSelection = 0;
    for (int i = 1; i < count; ++i) {
        if (optionsEnumArray[i] == static_cast<int>(config.lora.modem_preset)) {
            initialSelection = i;
            break;
        }
    }

    BannerOverlayOptions bannerOptions;
    bannerOptions.message = "Radio Preset";
    bannerOptions.optionsArrayPtr = optionsArray;
    bannerOptions.optionsEnumPtr = optionsEnumArray;
    bannerOptions.optionsCount = static_cast<uint8_t>(count);
    bannerOptions.InitialSelected = initialSelection;
    bannerOptions.bannerCallback = [](int selected) -> void {
        if (selected == -1) {
            menuHandler::menuQueue = menuHandler::LoraMenu;
            screen->runNow();
            return;
        }
        config.lora.use_preset = true;
        config.lora.modem_preset = static_cast<meshtastic_Config_LoRaConfig_ModemPreset>(selected);
        config.lora.channel_num = 0;        // Reset to default channel for the preset
        config.lora.override_frequency = 0; // Clear any custom frequency
        service->reloadConfig(SEGMENT_CONFIG);
    };
    return bannerOptions;
}

void menuHandler::radioPresetPicker()
{
    screen->showOverlayBanner(buildRegionPresetBanner());
}

void menuHandler::twelveHourPicker()
{
    static const char *optionsArray[] = {"Back", "12-hour", "24-hour"};
    enum optionsNumbers { Back = 0, twelve = 1, twentyfour = 2 };
    BannerOverlayOptions bannerOptions;
    bannerOptions.message = "Time Format";
    bannerOptions.optionsArrayPtr = optionsArray;
    bannerOptions.optionsCount = 3;
    bannerOptions.bannerCallback = [](int selected) -> void {
        if (selected == Back) {
            menuHandler::menuQueue = menuHandler::ClockMenu;
            screen->runNow();
        } else if (selected == twelve) {
            config.display.use_12h_clock = true;
        } else {
            config.display.use_12h_clock = false;
        }
        service->reloadConfig(SEGMENT_CONFIG);
    };
    screen->showOverlayBanner(bannerOptions);
}

// Reusable confirmation prompt function
void menuHandler::showConfirmationBanner(const char *message, std::function<void()> onConfirm)
{
    static const char *confirmOptions[] = {"No", "Yes"};
    BannerOverlayOptions confirmBanner;
    confirmBanner.message = message;
    confirmBanner.optionsArrayPtr = confirmOptions;
    confirmBanner.optionsCount = 2;
    confirmBanner.bannerCallback = [onConfirm](int confirmSelected) -> void {
        if (confirmSelected == 1) {
            onConfirm();
        }
    };
    screen->showOverlayBanner(confirmBanner);
}

void menuHandler::clockFacePicker()
{
    static const ClockFaceOption clockFaceOptions[] = {
        {"Back", OptionsAction::Back},
        {"Digital", OptionsAction::Select, false},
        {"Analog", OptionsAction::Select, true},
    };

    constexpr size_t clockFaceCount = sizeof(clockFaceOptions) / sizeof(clockFaceOptions[0]);
    static std::array<const char *, clockFaceCount> clockFaceLabels{};

    auto bannerOptions = createStaticBannerOptions("Which Face?", clockFaceOptions, clockFaceLabels,
                                                   [](const ClockFaceOption &option, int) -> void {
                                                       if (option.action == OptionsAction::Back) {
                                                           menuHandler::menuQueue = menuHandler::ClockMenu;
                                                           screen->runNow();
                                                           return;
                                                       }

                                                       if (!option.hasValue) {
                                                           return;
                                                       }

                                                       if (uiconfig.is_clockface_analog == option.value) {
                                                           return;
                                                       }

                                                       uiconfig.is_clockface_analog = option.value;
                                                       saveUIConfig();
                                                       screen->setFrames(Screen::FOCUS_CLOCK);
                                                   });

    bannerOptions.InitialSelected = uiconfig.is_clockface_analog ? 2 : 1;
    screen->showOverlayBanner(bannerOptions);
}

void menuHandler::TZPicker()
{
    static const TimezoneOption timezoneOptions[] = {
        {"Back", OptionsAction::Back},
        {"US/Hawaii", OptionsAction::Select, "HST10"},
        {"US/Alaska", OptionsAction::Select, "AKST9AKDT,M3.2.0,M11.1.0"},
        {"US/Pacific", OptionsAction::Select, "PST8PDT,M3.2.0,M11.1.0"},
        {"US/Arizona", OptionsAction::Select, "MST7"},
        {"US/Mountain", OptionsAction::Select, "MST7MDT,M3.2.0,M11.1.0"},
        {"US/Central", OptionsAction::Select, "CST6CDT,M3.2.0,M11.1.0"},
        {"US/Eastern", OptionsAction::Select, "EST5EDT,M3.2.0,M11.1.0"},
        {"BR/Brasilia", OptionsAction::Select, "BRT3"},
        {"UTC", OptionsAction::Select, "UTC0"},
        {"EU/Western", OptionsAction::Select, "GMT0BST,M3.5.0/1,M10.5.0"},
        {"EU/Central", OptionsAction::Select, "CET-1CEST,M3.5.0,M10.5.0/3"},
        {"EU/Eastern", OptionsAction::Select, "EET-2EEST,M3.5.0/3,M10.5.0/4"},
        {"Asia/Kolkata", OptionsAction::Select, "IST-5:30"},
        {"Asia/Hong_Kong", OptionsAction::Select, "HKT-8"},
        {"AU/AWST", OptionsAction::Select, "AWST-8"},
        {"AU/ACST", OptionsAction::Select, "ACST-9:30ACDT,M10.1.0,M4.1.0/3"},
        {"AU/AEST", OptionsAction::Select, "AEST-10AEDT,M10.1.0,M4.1.0/3"},
        {"Pacific/NZ", OptionsAction::Select, "NZST-12NZDT,M9.5.0,M4.1.0/3"},
    };

    constexpr size_t timezoneCount = sizeof(timezoneOptions) / sizeof(timezoneOptions[0]);
    static std::array<const char *, timezoneCount> timezoneLabels{};

    auto bannerOptions = createStaticBannerOptions(
        "Pick Timezone", timezoneOptions, timezoneLabels, [](const TimezoneOption &option, int) -> void {
            if (option.action == OptionsAction::Back) {
                menuHandler::menuQueue = menuHandler::ClockMenu;
                screen->runNow();
                return;
            }

            if (!option.hasValue) {
                return;
            }

            if (strncmp(config.device.tzdef, option.value, sizeof(config.device.tzdef)) == 0) {
                return;
            }

            strncpy(config.device.tzdef, option.value, sizeof(config.device.tzdef));
            config.device.tzdef[sizeof(config.device.tzdef) - 1] = '\0';

            setenv("TZ", config.device.tzdef, 1);
            service->reloadConfig(SEGMENT_CONFIG);
        });

    int initialSelection = 0;
    for (size_t i = 0; i < timezoneCount; ++i) {
        if (timezoneOptions[i].hasValue &&
            strncmp(config.device.tzdef, timezoneOptions[i].value, sizeof(config.device.tzdef)) == 0) {
            initialSelection = static_cast<int>(i);
            break;
        }
    }
    bannerOptions.InitialSelected = initialSelection;

    screen->showOverlayBanner(bannerOptions);
}

void menuHandler::clockMenu()
{
#if defined(OLED_TINY)
    static const char *optionsArray[] = {"Back", "Time Format", "Timezone"};
#else
    static const char *optionsArray[] = {"Back", "Clock Face", "Time Format", "Timezone"};
#endif
    enum optionsNumbers { Back = 0, Clock = 1, Time = 2, Timezone = 3 };
    BannerOverlayOptions bannerOptions;
    bannerOptions.message = "Clock Action";
    bannerOptions.optionsArrayPtr = optionsArray;
    bannerOptions.optionsCount = 4;
    bannerOptions.bannerCallback = [](int selected) -> void {
        if (selected == Clock) {
            menuHandler::menuQueue = menuHandler::ClockFacePicker;
            screen->runNow();
        } else if (selected == Time) {
            menuHandler::menuQueue = menuHandler::TwelveHourPicker;
            screen->runNow();
        } else if (selected == Timezone) {
            menuHandler::menuQueue = menuHandler::TzPicker;
            screen->runNow();
        }
    };
    screen->showOverlayBanner(bannerOptions);
}
void menuHandler::messageResponseMenu()
{
    enum optionsNumbers { Back = 0, ViewMode, DeleteMenu, ReplyMenu, MuteChannel, Aloud, enumEnd };

    static const char *optionsArray[enumEnd];
    static int optionsEnumArray[enumEnd];
    int options = 0;

    auto mode = graphics::MessageRenderer::getThreadMode();
    int threadChannel = graphics::MessageRenderer::getThreadChannel();

    optionsArray[options] = "Back";
    optionsEnumArray[options++] = Back;

    // New Reply submenu (replaces Preset and Freetext directly in this menu)
    optionsArray[options] = "Reply";
    optionsEnumArray[options++] = ReplyMenu;

    optionsArray[options] = "View Chats";
    optionsEnumArray[options++] = ViewMode;

    // If viewing ALL chats, hide “Mute Chat”
    if (mode != graphics::MessageRenderer::ThreadMode::ALL && mode != graphics::MessageRenderer::ThreadMode::DIRECT) {
        const uint8_t chIndex = (threadChannel != 0) ? (uint8_t)threadChannel : channels.getPrimaryIndex();
        const auto &chan = channels.getByIndex(chIndex);

        optionsArray[options] = chan.settings.module_settings.is_muted ? "Unmute Channel" : "Mute Channel";
        optionsEnumArray[options++] = MuteChannel;
    }

    // Delete submenu
    optionsArray[options] = "Delete";
    optionsEnumArray[options++] = DeleteMenu;

#ifdef HAS_I2S
    optionsArray[options] = "Read Aloud";
    optionsEnumArray[options++] = Aloud;
#endif

    BannerOverlayOptions bannerOptions;
    bannerOptions.message = "Message Action";
    if (currentResolution == ScreenResolution::UltraLow) {
        bannerOptions.message = "Message";
    }
    bannerOptions.optionsArrayPtr = optionsArray;
    bannerOptions.optionsEnumPtr = optionsEnumArray;
    bannerOptions.optionsCount = options;
    bannerOptions.bannerCallback = [](int selected) -> void {
        LOG_DEBUG("messageResponseMenu: selected %d", selected);

        auto mode = graphics::MessageRenderer::getThreadMode();
        int ch = graphics::MessageRenderer::getThreadChannel();
        uint32_t peer = graphics::MessageRenderer::getThreadPeer();

        LOG_DEBUG("[ReplyCtx] mode=%d ch=%d peer=0x%08x", (int)mode, ch, (unsigned int)peer);

        if (selected == ViewMode) {
            menuHandler::menuQueue = menuHandler::MessageViewModeMenu;
            screen->runNow();

            // Reply submenu
        } else if (selected == ReplyMenu) {
            menuHandler::menuQueue = menuHandler::ReplyMenu;
            screen->runNow();

        } else if (selected == MuteChannel) {
            const uint8_t chIndex = (ch != 0) ? (uint8_t)ch : channels.getPrimaryIndex();
            auto &chan = channels.getByIndex(chIndex);
            if (chan.settings.has_module_settings) {
                chan.settings.module_settings.is_muted = !chan.settings.module_settings.is_muted;
                nodeDB->saveToDisk();
            }

        } else if (selected == DeleteMenu) {
            menuHandler::menuQueue = menuHandler::DeleteMessagesMenu;
            screen->runNow();

#ifdef HAS_I2S
        } else if (selected == Aloud) {
            if (const StoredMessage *latest = getNewestMessageForActiveThread()) {
                const char *msg = MessageStore::getText(*latest);
                if (msg && msg[0]) {
                    audioThread->readAloud(msg);
                }
            }
#endif
        }
    };
    screen->showOverlayBanner(bannerOptions);
}

void menuHandler::replyMenu()
{
    enum replyOptions { Back = 0, ReplyPreset, ReplyFreetext, enumEnd };

    static const char *optionsArray[enumEnd];
    static int optionsEnumArray[enumEnd];
    int options = 0;

    // Back
    optionsArray[options] = "Back";
    optionsEnumArray[options++] = Back;

    // Preset reply
    optionsArray[options] = "With Preset";
    optionsEnumArray[options++] = ReplyPreset;

    // Freetext reply (only when keyboard exists)
    if (kb_found) {
        optionsArray[options] = "With Freetext";
        optionsEnumArray[options++] = ReplyFreetext;
    }

    BannerOverlayOptions bannerOptions;

    // Dynamic title based on thread mode
    auto mode = graphics::MessageRenderer::getThreadMode();
    if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) {
        bannerOptions.message = "Reply to Channel";
    } else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) {
        bannerOptions.message = "Reply to DM";
    } else {
        // View All
        bannerOptions.message = "Reply to Last Msg";
    }

    bannerOptions.optionsArrayPtr = optionsArray;
    bannerOptions.optionsEnumPtr = optionsEnumArray;
    bannerOptions.optionsCount = options;
    bannerOptions.InitialSelected = 1;

    bannerOptions.bannerCallback = [](int selected) -> void {
        auto mode = graphics::MessageRenderer::getThreadMode();
        int ch = graphics::MessageRenderer::getThreadChannel();
        uint32_t peer = graphics::MessageRenderer::getThreadPeer();

        if (selected == Back) {
            menuHandler::menuQueue = menuHandler::MessageResponseMenu;
            screen->runNow();
            return;
        }

        // Preset reply
        if (selected == ReplyPreset) {
            if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) {
                cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST, ch);
            } else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) {
                cannedMessageModule->LaunchWithDestination(peer);
            } else if (const StoredMessage *latest = getNewestMessageForActiveThread()) {
                launchReplyForMessage(*latest, false);
            }

            return;
        }

        // Freetext reply
        if (selected == ReplyFreetext) {
            if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) {
                cannedMessageModule->LaunchFreetextWithDestination(NODENUM_BROADCAST, ch);
            } else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) {
                cannedMessageModule->LaunchFreetextWithDestination(peer);
            } else if (const StoredMessage *latest = getNewestMessageForActiveThread()) {
                launchReplyForMessage(*latest, true);
            }

            return;
        }
    };
    screen->showOverlayBanner(bannerOptions);
}
void menuHandler::deleteMessagesMenu()
{
    enum optionsNumbers { Back = 0, DeleteOldest, DeleteThis, DeleteAll, enumEnd };

    static const char *optionsArray[enumEnd];
    static int optionsEnumArray[enumEnd];
    int options = 0;

    auto mode = graphics::MessageRenderer::getThreadMode();

    optionsArray[options] = "Back";
    optionsEnumArray[options++] = Back;

    optionsArray[options] = "Delete Oldest";
    optionsEnumArray[options++] = DeleteOldest;

    // If viewing ALL chats → hide “Delete This Chat”
    if (mode != graphics::MessageRenderer::ThreadMode::ALL) {
        optionsArray[options] = "Delete This Chat";
        optionsEnumArray[options++] = DeleteThis;
    }
    if (currentResolution == ScreenResolution::UltraLow) {
        optionsArray[options] = "Delete All";
    } else {
        optionsArray[options] = "Delete All Chats";
    }
    optionsEnumArray[options++] = DeleteAll;

    BannerOverlayOptions bannerOptions;
    bannerOptions.message = "Delete Messages";

    bannerOptions.optionsArrayPtr = optionsArray;
    bannerOptions.optionsEnumPtr = optionsEnumArray;
    bannerOptions.optionsCount = options;
    bannerOptions.bannerCallback = [mode](int selected) -> void {
        int ch = graphics::MessageRenderer::getThreadChannel();
        uint32_t peer = graphics::MessageRenderer::getThreadPeer();

        if (selected == Back) {
            menuHandler::menuQueue = menuHandler::MessageResponseMenu;
            screen->runNow();
            return;
        }

        if (selected == DeleteAll) {
            LOG_INFO("Deleting all messages");
            messageStore.clearAllMessages();
            graphics::MessageRenderer::clearThreadRegistries();
            graphics::MessageRenderer::clearMessageCache();
            return;
        }

        if (selected == DeleteOldest) {
            LOG_INFO("Deleting oldest message");

            if (mode == graphics::MessageRenderer::ThreadMode::ALL) {
                messageStore.deleteOldestMessage();
            } else if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) {
                messageStore.deleteOldestMessageInChannel(ch);
            } else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) {
                messageStore.deleteOldestMessageWithPeer(peer);
            }
            return;
        }

        // This only appears in non-ALL modes
        if (selected == DeleteThis) {
            LOG_INFO("Deleting all messages in this thread");

            if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) {
                messageStore.deleteAllMessagesInChannel(ch);
            } else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) {
                messageStore.deleteAllMessagesWithPeer(peer);
            }
            return;
        }
    };

    screen->showOverlayBanner(bannerOptions);
}

void menuHandler::messageViewModeMenu()
{
    auto encodeChannelId = [](int ch) -> int { return 100 + ch; };
    auto isChannelSel = [](int id) -> bool { return id >= 100 && id < 200; };

    static std::vector<std::string> labels;
    static std::vector<int> ids;
    static std::vector<uint32_t> idToPeer; // DM lookup

    labels.clear();
    ids.clear();
    idToPeer.clear();

    labels.push_back("Back");
    ids.push_back(-1);
    labels.push_back("View All Chats");
    ids.push_back(-2);

    // Channels with messages
    for (int ch = 0; ch < 8; ++ch) {
        auto msgs = messageStore.getChannelMessages((uint8_t)ch);
        if (!msgs.empty()) {
            char buf[40];
            const char *cname = channels.getName(ch);
            snprintf(buf, sizeof(buf), cname && cname[0] ? "#%s" : "#Ch%d", cname ? cname : "", ch);
            labels.push_back(buf);
            ids.push_back(encodeChannelId(ch));
            LOG_DEBUG("messageViewModeMenu: Added live channel %s (id=%d)", buf, encodeChannelId(ch));
        }
    }

    // Registry channels
    for (int ch : graphics::MessageRenderer::getSeenChannels()) {
        if (ch < 0 || ch >= 8)
            continue;
        auto msgs = messageStore.getChannelMessages((uint8_t)ch);
        if (msgs.empty())
            continue;
        int enc = encodeChannelId(ch);
        if (std::find(ids.begin(), ids.end(), enc) == ids.end()) {
            char buf[40];
            const char *cname = channels.getName(ch);
            snprintf(buf, sizeof(buf), cname && cname[0] ? "#%s" : "#Ch%d", cname ? cname : "", ch);
            labels.push_back(buf);
            ids.push_back(enc);
            LOG_DEBUG("messageViewModeMenu: Added registry channel %s (id=%d)", buf, enc);
        }
    }

    // Gather unique peers
    auto dms = messageStore.getDirectMessages();
    std::vector<uint32_t> uniquePeers;
    for (const auto &m : dms) {
        uint32_t peer = (m.sender == nodeDB->getNodeNum()) ? m.dest : m.sender;
        if (peer != nodeDB->getNodeNum() && std::find(uniquePeers.begin(), uniquePeers.end(), peer) == uniquePeers.end())
            uniquePeers.push_back(peer);
    }
    for (uint32_t peer : graphics::MessageRenderer::getSeenPeers()) {
        if (peer != nodeDB->getNodeNum() && std::find(uniquePeers.begin(), uniquePeers.end(), peer) == uniquePeers.end())
            uniquePeers.push_back(peer);
    }
    std::sort(uniquePeers.begin(), uniquePeers.end());

    // Encode peers
    for (size_t i = 0; i < uniquePeers.size(); ++i) {
        uint32_t peer = uniquePeers[i];
        const auto *node = nodeDB->getMeshNode(peer);
        std::string name;
        if (nodeInfoLiteHasUser(node))
            name = sanitizeString(node->long_name).substr(0, 15);
        else {
            char buf[20];
            snprintf(buf, sizeof(buf), "Node %08X", peer);
            name = buf;
        }
        labels.push_back("@" + name);
        int encPeer = 1000 + (int)idToPeer.size();
        ids.push_back(encPeer);
        idToPeer.push_back(peer);
        LOG_DEBUG("messageViewModeMenu: Added DM %s peer=0x%08x id=%d", name.c_str(), (unsigned int)peer, encPeer);
    }

    // Active ID
    int activeId = -2;
    auto mode = graphics::MessageRenderer::getThreadMode();
    if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL)
        activeId = encodeChannelId(graphics::MessageRenderer::getThreadChannel());
    else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) {
        uint32_t cur = graphics::MessageRenderer::getThreadPeer();
        for (size_t i = 0; i < idToPeer.size(); ++i)
            if (idToPeer[i] == cur) {
                activeId = 1000 + (int)i;
                break;
            }
    }

    LOG_DEBUG("messageViewModeMenu: Active thread id=%d", activeId);

    // Build banner
    static std::vector<const char *> options;
    static std::vector<int> optionIds;
    options.clear();
    optionIds.clear();

    int initialIndex = 0;
    for (size_t i = 0; i < labels.size(); i++) {
        options.push_back(labels[i].c_str());
        optionIds.push_back(ids[i]);
        if (ids[i] == activeId)
            initialIndex = (int)i;
    }

    BannerOverlayOptions bannerOptions;
    bannerOptions.message = "Select Conversation";
    bannerOptions.optionsArrayPtr = options.data();
    bannerOptions.optionsEnumPtr = optionIds.data();
    bannerOptions.optionsCount = options.size();
    bannerOptions.InitialSelected = initialIndex;

    bannerOptions.bannerCallback = [=](int selected) -> void {
        LOG_DEBUG("messageViewModeMenu: selected=%d", selected);
        if (selected == -1) {
            menuHandler::menuQueue = menuHandler::MessageResponseMenu;
            screen->runNow();
        } else if (selected == -2) {
            graphics::MessageRenderer::setThreadMode(graphics::MessageRenderer::ThreadMode::ALL);
        } else if (isChannelSel(selected)) {
            int ch = selected - 100;
            graphics::MessageRenderer::setThreadMode(graphics::MessageRenderer::ThreadMode::CHANNEL, ch);
        } else if (selected >= 1000) {
            int idx = selected - 1000;
            if (idx >= 0 && (size_t)idx < idToPeer.size()) {
                uint32_t peer = idToPeer[idx];
                graphics::MessageRenderer::setThreadMode(graphics::MessageRenderer::ThreadMode::DIRECT, -1, peer);
            }
        }
    };
    screen->showOverlayBanner(bannerOptions);
}

void menuHandler::homeBaseMenu()
{
    enum optionsNumbers { Back, Mute, Backlight, Position, Preset, Freetext, Sleep, enumEnd };

    static const char *optionsArray[enumEnd] = {"Back"};
    static int optionsEnumArray[enumEnd] = {Back};
    int options = 1;

    if (moduleConfig.external_notification.enabled && externalNotificationModule &&
        config.device.buzzer_mode != meshtastic_Config_DeviceConfig_BuzzerMode_DISABLED) {
        if (!externalNotificationModule->getMute()) {
            optionsArray[options] = "Temporarily Mute";
        } else {
            optionsArray[options] = "Unmute";
        }
        optionsEnumArray[options++] = Mute;
    }
#if defined(PIN_EINK_EN) || defined(PCA_PIN_EINK_EN)
    optionsArray[options] = "Toggle Backlight";
    optionsEnumArray[options++] = Backlight;
#else
    optionsArray[options] = "Sleep Screen";
    optionsEnumArray[options++] = Sleep;
#endif
    if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED) {
        optionsArray[options] = "Send Position";
    } else {
        optionsArray[options] = "Send Node Info";
    }
    optionsEnumArray[options++] = Position;

    BannerOverlayOptions bannerOptions;
    bannerOptions.message = "Home Action";
    if (currentResolution == ScreenResolution::UltraLow) {
        bannerOptions.message = "Home";
    }
    bannerOptions.optionsArrayPtr = optionsArray;
    bannerOptions.optionsEnumPtr = optionsEnumArray;
    bannerOptions.optionsCount = options;
    bannerOptions.bannerCallback = [](int selected) -> void {
        if (selected == Mute) {
            if (moduleConfig.external_notification.enabled && externalNotificationModule) {
                externalNotificationModule->setMute(!externalNotificationModule->getMute());
                IF_SCREEN(if (!externalNotificationModule->getMute()) externalNotificationModule->stopNow();)
            }
        } else if (selected == Backlight) {
            screen->setOn(false);
#if defined(PIN_EINK_EN)
            if (uiconfig.screen_brightness == 1) {
                uiconfig.screen_brightness = 0;
                digitalWrite(PIN_EINK_EN, LOW);
            } else {
                uiconfig.screen_brightness = 1;
                digitalWrite(PIN_EINK_EN, HIGH);
            }
            saveUIConfig();
#elif defined(PCA_PIN_EINK_EN)
            if (uiconfig.screen_brightness > 0) {
                uiconfig.screen_brightness = 0;
                io.digitalWrite(PCA_PIN_EINK_EN, LOW);
            } else {
                uiconfig.screen_brightness = 1;
                io.digitalWrite(PCA_PIN_EINK_EN, HIGH);
            }
            saveUIConfig();
#endif
        } else if (selected == Sleep) {
            screen->setOn(false);
        } else if (selected == Position) {
            service->refreshLocalMeshNode();
            if (service->trySendPosition(NODENUM_BROADCAST, true)) {
                IF_SCREEN(screen->showSimpleBanner("Position\nSent", 3000));
            } else {
                IF_SCREEN(screen->showSimpleBanner("Node Info\nSent", 3000));
            }
        } else if (selected == Preset) {
            cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST);
        } else if (selected == Freetext) {
            cannedMessageModule->LaunchFreetextWithDestination(NODENUM_BROADCAST);
        }
    };
    screen->showOverlayBanner(bannerOptions);
}

void menuHandler::textMessageMenu()
{
    cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST);
}

void menuHandler::textMessageBaseMenu()
{
    enum optionsNumbers { Back, Preset, Freetext, enumEnd };

    static const char *optionsArray[enumEnd] = {"Back"};
    static int optionsEnumArray[enumEnd] = {Back};
    int options = 1;
    optionsArray[options] = "New Preset Msg";
    optionsEnumArray[options++] = Preset;
    if (kb_found) {
        optionsArray[options] = "New Freetext Msg";
        optionsEnumArray[options++] = Freetext;
    }

    BannerOverlayOptions bannerOptions;
    bannerOptions.message = "Message Action";
    bannerOptions.optionsArrayPtr = optionsArray;
    bannerOptions.optionsEnumPtr = optionsEnumArray;
    bannerOptions.optionsCount = options;
    bannerOptions.bannerCallback = [](int selected) -> void {
        if (selected == Preset) {
            cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST);
        } else if (selected == Freetext) {
            cannedMessageModule->LaunchFreetextWithDestination(NODENUM_BROADCAST);
        }
    };
    screen->showOverlayBanner(bannerOptions);
}

void menuHandler::systemBaseMenu()
{
    enum optionsNumbers { Back, Notifications, ScreenOptions, Bluetooth, WiFiToggle, PowerMenu, Test, enumEnd };
    static const char *optionsArray[enumEnd] = {"Back"};
    static int optionsEnumArray[enumEnd] = {Back};
    int options = 1;

    optionsArray[options] = "Notifications";
    optionsEnumArray[options++] = Notifications;

    optionsArray[options] = "Display Options";
    optionsEnumArray[options++] = ScreenOptions;

    if (currentResolution == ScreenResolution::UltraLow) {
        optionsArray[options] = "Bluetooth";
    } else {
        optionsArray[options] = "Bluetooth Toggle";
    }
    optionsEnumArray[options++] = Bluetooth;
#if HAS_WIFI && !defined(ARCH_PORTDUINO)
    optionsArray[options] = "WiFi Toggle";
    optionsEnumArray[options++] = WiFiToggle;
#endif

    if (currentResolution == ScreenResolution::UltraLow) {
        optionsArray[options] = "Power";
    } else {
        optionsArray[options] = "Reboot/Shutdown";
    }
    optionsEnumArray[options++] = PowerMenu;

    if (test_enabled) {
        optionsArray[options] = "Test Menu";
        optionsEnumArray[options++] = Test;
    }

    BannerOverlayOptions bannerOptions;
    bannerOptions.message = "System Action";
    if (currentResolution == ScreenResolution::UltraLow) {
        bannerOptions.message = "System";
    }
    bannerOptions.optionsArrayPtr = optionsArray;
    bannerOptions.optionsCount = options;
    bannerOptions.optionsEnumPtr = optionsEnumArray;
    bannerOptions.bannerCallback = [](int selected) -> void {
        if (selected == Notifications) {
            menuHandler::menuQueue = menuHandler::BuzzerModeMenuPicker;
            screen->runNow();
        } else if (selected == ScreenOptions) {
            menuHandler::menuQueue = menuHandler::ScreenOptionsMenu;
            screen->runNow();
        } else if (selected == PowerMenu) {
            menuHandler::menuQueue = menuHandler::PowerMenu;
            screen->runNow();
        } else if (selected == Test) {
            menuHandler::menuQueue = menuHandler::TestMenu;
            screen->runNow();
        } else if (selected == Bluetooth) {
            menuQueue = BluetoothToggleMenu;
            screen->runNow();
#if HAS_WIFI && !defined(ARCH_PORTDUINO)
        } else if (selected == WiFiToggle) {
            menuQueue = WifiToggleMenu;
            screen->runNow();
#endif
        } else if (selected == Back && !test_enabled) {
            test_count++;
            if (test_count > 4) {
                test_enabled = true;
            }
        }
    };
    screen->showOverlayBanner(bannerOptions);
}

void menuHandler::favoriteBaseMenu()
{
    enum optionsNumbers { Back, Preset, Freetext, GoToChat, Remove, TraceRoute, enumEnd };

    static const char *optionsArray[enumEnd] = {"Back"};
    static int optionsEnumArray[enumEnd] = {Back};
    int options = 1;

    // Only show "View Conversation" if a message exists with this node
    uint32_t peer = graphics::UIRenderer::currentFavoriteNodeNum;
    bool hasConversation = false;
    for (const auto &m : messageStore.getMessages()) {
        if ((m.sender == peer || m.dest == peer)) {
            hasConversation = true;
            break;
        }
    }
    if (hasConversation) {
        optionsArray[options] = "Go To Chat";
        optionsEnumArray[options++] = GoToChat;
    }
    if (currentResolution == ScreenResolution::UltraLow) {
        optionsArray[options] = "New Preset";
    } else {
        optionsArray[options] = "New Preset Msg";
    }
    optionsEnumArray[options++] = Preset;

    if (kb_found) {
        optionsArray[options] = "New Freetext Msg";
        optionsEnumArray[options++] = Freetext;
    }

    if (currentResolution != ScreenResolution::UltraLow) {
        optionsArray[options] = "Trace Route";
        optionsEnumArray[options++] = TraceRoute;
    }
    optionsArray[options] = "Remove Favorite";
    optionsEnumArray[options++] = Remove;

    BannerOverlayOptions bannerOptions;
    bannerOptions.message = "Favorites Action";
    if (currentResolution == ScreenResolution::UltraLow) {
        bannerOptions.message = "Favorites";
    }
    bannerOptions.optionsArrayPtr = optionsArray;
    bannerOptions.optionsEnumPtr = optionsEnumArray;
    bannerOptions.optionsCount = options;
    bannerOptions.bannerCallback = [](int selected) -> void {
        if (selected == Preset) {
            cannedMessageModule->LaunchWithDestination(graphics::UIRenderer::currentFavoriteNodeNum);
        } else if (selected == Freetext) {
            cannedMessageModule->LaunchFreetextWithDestination(graphics::UIRenderer::currentFavoriteNodeNum);
        }
        // Handle new Go To Thread action
        else if (selected == GoToChat) {
            // Switch thread to direct conversation with this node
            graphics::MessageRenderer::setThreadMode(graphics::MessageRenderer::ThreadMode::DIRECT, -1,
                                                     graphics::UIRenderer::currentFavoriteNodeNum);

            // Manually create and send a UIFrameEvent to trigger the jump
            UIFrameEvent evt;
            evt.action = UIFrameEvent::Action::SWITCH_TO_TEXTMESSAGE;
            screen->handleUIFrameEvent(&evt);
        } else if (selected == Remove) {
            menuHandler::menuQueue = menuHandler::RemoveFavorite;
            screen->runNow();
        } else if (selected == TraceRoute) {
            if (traceRouteModule) {
                traceRouteModule->launch(graphics::UIRenderer::currentFavoriteNodeNum);
            }
        }
    };
    screen->showOverlayBanner(bannerOptions);
}

void menuHandler::positionBaseMenu()
{
    enum class PositionAction {
        GpsToggle,
        GpsFormat,
        CompassMenu,
        CompassCalibrate,
        GPSSmartPosition,
        GPSUpdateInterval,
        GPSPositionBroadcast
    };

    static const PositionMenuOption baseOptions[] = {
        {"Back", OptionsAction::Back},
        {"On/Off Toggle", OptionsAction::Select, static_cast<int>(PositionAction::GpsToggle)},
        {"Format", OptionsAction::Select, static_cast<int>(PositionAction::GpsFormat)},
        {"Smart Position", OptionsAction::Select, static_cast<int>(PositionAction::GPSSmartPosition)},
        {"Update Interval", OptionsAction::Select, static_cast<int>(PositionAction::GPSUpdateInterval)},
        {"Broadcast Interval", OptionsAction::Select, static_cast<int>(PositionAction::GPSPositionBroadcast)},
        {"Compass", OptionsAction::Select, static_cast<int>(PositionAction::CompassMenu)},
    };

    static const PositionMenuOption calibrateOptions[] = {
        {"Back", OptionsAction::Back},
        {"On/Off Toggle", OptionsAction::Select, static_cast<int>(PositionAction::GpsToggle)},
        {"Format", OptionsAction::Select, static_cast<int>(PositionAction::GpsFormat)},
        {"Smart Position", OptionsAction::Select, static_cast<int>(PositionAction::GPSSmartPosition)},
        {"Update Interval", OptionsAction::Select, static_cast<int>(PositionAction::GPSUpdateInterval)},
        {"Broadcast Interval", OptionsAction::Select, static_cast<int>(PositionAction::GPSPositionBroadcast)},
        {"Compass", OptionsAction::Select, static_cast<int>(PositionAction::CompassMenu)},
        {"Compass Calibrate", OptionsAction::Select, static_cast<int>(PositionAction::CompassCalibrate)},
    };

    constexpr size_t baseCount = sizeof(baseOptions) / sizeof(baseOptions[0]);
    static std::array<const char *, baseCount> baseLabels{};
#if !MESHTASTIC_EXCLUDE_ACCELEROMETER
    constexpr size_t calibrateCount = sizeof(calibrateOptions) / sizeof(calibrateOptions[0]);
    static std::array<const char *, calibrateCount> calibrateLabels{};
#endif

    auto onSelection = [](const PositionMenuOption &option, int) -> void {
        if (option.action == OptionsAction::Back) {
            return;
        }

        if (!option.hasValue) {
            return;
        }

        auto action = static_cast<PositionAction>(option.value);
        switch (action) {
        case PositionAction::GpsToggle:
            menuQueue = GpsToggleMenu;
            screen->runNow();
            break;
        case PositionAction::GpsFormat:
            menuQueue = GpsFormatMenu;
            screen->runNow();
            break;
        case PositionAction::CompassMenu:
            menuQueue = CompassPointNorthMenu;
            screen->runNow();
            break;
        case PositionAction::CompassCalibrate:
#if !MESHTASTIC_EXCLUDE_ACCELEROMETER
            if (accelerometerThread) {
                accelerometerThread->calibrate(30);
            }
#endif
#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C && !MESHTASTIC_EXCLUDE_MAGNETOMETER
            if (magnetometerThread) {
                magnetometerThread->calibrate(30);
            }
#endif
            break;
        case PositionAction::GPSSmartPosition:
            menuQueue = GpsSmartPositionMenu;
            screen->runNow();
            break;
        case PositionAction::GPSUpdateInterval:
            menuQueue = GpsUpdateIntervalMenu;
            screen->runNow();
            break;
        case PositionAction::GPSPositionBroadcast:
            menuQueue = GpsPositionBroadcastMenu;
            screen->runNow();
            break;
        }
    };

    BannerOverlayOptions bannerOptions;
#if !MESHTASTIC_EXCLUDE_ACCELEROMETER
    if (accelerometerThread) {
        bannerOptions = createStaticBannerOptions("GPS Action", calibrateOptions, calibrateLabels, onSelection);
    } else {
        bannerOptions = createStaticBannerOptions("GPS Action", baseOptions, baseLabels, onSelection);
    }
#else
    bannerOptions = createStaticBannerOptions("GPS Action", baseOptions, baseLabels, onSelection);
#endif

    screen->showOverlayBanner(bannerOptions);
}

void menuHandler::nodeListMenu()
{
    enum optionsNumbers { Back, NodePicker, TraceRoute, Verify, Reset, NodeNameLength, enumEnd };
    static const char *optionsArray[enumEnd] = {"Back"};
    static int optionsEnumArray[enumEnd] = {Back};
    int options = 1;

    optionsArray[options] = "Node Actions / Settings";
    optionsEnumArray[options++] = NodePicker;

    if (currentResolution != ScreenResolution::UltraLow) {
        optionsArray[options] = "Show Long/Short Name";
        optionsEnumArray[options++] = NodeNameLength;
    }
    optionsArray[options] = "Reset NodeDB";
    optionsEnumArray[options++] = Reset;

    BannerOverlayOptions bannerOptions;
    bannerOptions.message = "Node Action";
    bannerOptions.optionsArrayPtr = optionsArray;
    bannerOptions.optionsCount = options;
    bannerOptions.optionsEnumPtr = optionsEnumArray;
    bannerOptions.bannerCallback = [](int selected) -> void {
        if (selected == NodePicker) {
            menuQueue = NodePickerMenu;
            screen->runNow();
        } else if (selected == Reset) {
            menuQueue = ResetNodeDbMenu;
            screen->runNow();
        } else if (selected == NodeNameLength) {
            menuHandler::menuQueue = menuHandler::NodeNameLengthMenu;
            screen->runNow();
        }
    };
    screen->showOverlayBanner(bannerOptions);
}

void menuHandler::NodePicker()
{
    const char *NODE_PICKER_TITLE;
    if (currentResolution == ScreenResolution::UltraLow) {
        NODE_PICKER_TITLE = "Pick Node";
    } else {
        NODE_PICKER_TITLE = "Pick A Node";
    }
    screen->showNodePicker(NODE_PICKER_TITLE, 30000, [](uint32_t nodenum) -> void {
        LOG_INFO("Nodenum: %u", nodenum);
        // Store the selection so the Manage Node menu knows which node to operate on
        menuHandler::pickedNodeNum = nodenum;
        // Keep UI favorite context in sync (used elsewhere for some node-based actions)
        graphics::UIRenderer::currentFavoriteNodeNum = nodenum;
        menuQueue = ManageNodeMenu;
        screen->runNow();
    });
}

void menuHandler::manageNodeMenu()
{
    // If we don't have a node selected yet, go fast exit
    auto node = nodeDB->getMeshNode(menuHandler::pickedNodeNum);
    if (!node) {
        return;
    }
    enum optionsNumbers { Back, Favorite, Mute, TraceRoute, KeyVerification, Ignore, enumEnd };
    static const char *optionsArray[enumEnd] = {"Back"};
    static int optionsEnumArray[enumEnd] = {Back};
    int options = 1;

    if (nodeInfoLiteIsFavorite(node)) {
        optionsArray[options] = "Unfavorite";
    } else {
        optionsArray[options] = "Favorite";
    }
    optionsEnumArray[options++] = Favorite;

    bool isMuted = nodeInfoLiteIsMuted(node);
    if (isMuted) {
        optionsArray[options] = "Unmute Notifications";
    } else {
        optionsArray[options] = "Mute Notifications";
    }
    optionsEnumArray[options++] = Mute;

    optionsArray[options] = "Trace Route";
    optionsEnumArray[options++] = TraceRoute;

    optionsArray[options] = "Key Verification";
    optionsEnumArray[options++] = KeyVerification;

    if (nodeInfoLiteIsIgnored(node)) {
        optionsArray[options] = "Unignore Node";
    } else {
        optionsArray[options] = "Ignore Node";
    }
    optionsEnumArray[options++] = Ignore;

    BannerOverlayOptions bannerOptions;

    std::string title = "";
    if (nodeInfoLiteHasUser(node) && node->long_name[0]) {
        title += sanitizeString(node->long_name).substr(0, 15);
    } else {
        char buf[20];
        snprintf(buf, sizeof(buf), "%08X", (unsigned int)node->num);
        title += buf;
    }
    bannerOptions.message = title.c_str();
    bannerOptions.optionsArrayPtr = optionsArray;
    bannerOptions.optionsCount = options;
    bannerOptions.optionsEnumPtr = optionsEnumArray;
    bannerOptions.bannerCallback = [](int selected) -> void {
        if (selected == Back) {
            menuQueue = NodeBaseMenu;
            screen->runNow();
            return;
        }

        if (selected == Favorite) {
            const auto *n = nodeDB->getMeshNode(menuHandler::pickedNodeNum);
            if (!n) {
                return;
            }
            if (nodeInfoLiteIsFavorite(n)) {
                LOG_INFO("Removing node %08X from favorites", menuHandler::pickedNodeNum);
                nodeDB->set_favorite(false, menuHandler::pickedNodeNum);
            } else {
                LOG_INFO("Adding node %08X to favorites", menuHandler::pickedNodeNum);
                // set_favorite() already logs PROTECTED_CAP_WARN_FMT on a cap refusal; don't double-log here.
                nodeDB->set_favorite(true, menuHandler::pickedNodeNum);
            }
            screen->setFrames(graphics::Screen::FOCUS_PRESERVE);
            return;
        }

        if (selected == Mute) {
            auto n = nodeDB->getMeshNode(menuHandler::pickedNodeNum);
            if (!n) {
                return;
            }

            const bool wasMuted = nodeInfoLiteIsMuted(n);
            nodeInfoLiteSetBit(n, NODEINFO_BITFIELD_IS_MUTED_MASK, !wasMuted);
            LOG_INFO(wasMuted ? "Unmuted node %08X" : "Muted node %08X", menuHandler::pickedNodeNum);
            nodeDB->notifyObservers(true);
            nodeDB->saveToDisk();
            screen->setFrames(graphics::Screen::FOCUS_PRESERVE);
            return;
        }

        if (selected == TraceRoute) {
            LOG_INFO("Starting traceroute to %08X", menuHandler::pickedNodeNum);
            if (traceRouteModule) {
                traceRouteModule->startTraceRoute(menuHandler::pickedNodeNum);
            }
            return;
        }

        if (selected == KeyVerification) {
            LOG_INFO("Initiating key verification with %08X", menuHandler::pickedNodeNum);
            if (keyVerificationModule) {
                keyVerificationModule->sendInitialRequest(menuHandler::pickedNodeNum);
            }
            return;
        }

        if (selected == Ignore) {
            auto n = nodeDB->getMeshNode(menuHandler::pickedNodeNum);
            if (!n) {
                return;
            }

            bool changed = false;
            if (nodeInfoLiteIsIgnored(n)) {
                nodeInfoLiteSetBit(n, NODEINFO_BITFIELD_IS_IGNORED_MASK, false);
                LOG_INFO("Unignoring node %08X", menuHandler::pickedNodeNum);
                changed = true;
            } else if (nodeDB->setProtectedFlag(n, NODEINFO_BITFIELD_IS_IGNORED_MASK, true)) {
                LOG_INFO("Ignoring node %08X", menuHandler::pickedNodeNum);
                changed = true;
            } else {
                LOG_WARN(NodeDB::PROTECTED_CAP_WARN_FMT, "ignore", menuHandler::pickedNodeNum, MAX_NUM_NODES - 2);
            }
            // Only persist/notify when the ignore bit actually moved; a cap
            // refusal changed nothing and shouldn't trigger a prefs save.
            if (changed) {
                nodeDB->notifyObservers(true);
                nodeDB->saveToDisk();
            }
            screen->setFrames(graphics::Screen::FOCUS_PRESERVE);
            return;
        }
    };
    screen->showOverlayBanner(bannerOptions);
}

void menuHandler::nodeNameLengthMenu()
{
    static const NodeNameOption nodeNameOptions[] = {
        {"Back", OptionsAction::Back},
        {"Long", OptionsAction::Select, true},
        {"Short", OptionsAction::Select, false},
    };

    constexpr size_t nodeNameCount = sizeof(nodeNameOptions) / sizeof(nodeNameOptions[0]);
    static std::array<const char *, nodeNameCount> nodeNameLabels{};

    auto bannerOptions = createStaticBannerOptions("Node Name Length", nodeNameOptions, nodeNameLabels,
                                                   [](const NodeNameOption &option, int) -> void {
                                                       if (option.action == OptionsAction::Back) {
                                                           menuQueue = NodeBaseMenu;
                                                           screen->runNow();
                                                           return;
                                                       }

                                                       if (!option.hasValue) {
                                                           return;
                                                       }

                                                       if (config.display.use_long_node_name == option.value) {
                                                           return;
                                                       }

                                                       config.display.use_long_node_name = option.value;
                                                       saveUIConfig();
                                                       service->reloadConfig(SEGMENT_CONFIG);
                                                       LOG_INFO("Setting names to %s", option.value ? "long" : "short");
                                                   });

    int initialSelection = config.display.use_long_node_name ? 1 : 2;
    bannerOptions.InitialSelected = initialSelection;

    screen->showOverlayBanner(bannerOptions);
}

void menuHandler::resetNodeDBMenu()
{
    static const char *optionsArray[] = {"Back", "Reset All", "Preserve Favorites"};
    BannerOverlayOptions bannerOptions;
    bannerOptions.message = "Confirm Reset NodeDB";
    bannerOptions.optionsArrayPtr = optionsArray;
    bannerOptions.optionsCount = 3;
    bannerOptions.bannerCallback = [](int selected) -> void {
        if (selected == 1 || selected == 2) {
            disableBluetooth();
            screen->setFrames(Screen::FOCUS_DEFAULT);
        }
        if (selected == 1) {
            LOG_INFO("Initiate node-db reset");
            nodeDB->resetNodes();
            rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000);
        } else if (selected == 2) {
            LOG_INFO("Initiate node-db reset but keeping favorites");
            nodeDB->resetNodes(1);
            rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000);
        } else if (selected == 0) {
            menuQueue = NodeBaseMenu;
            screen->runNow();
        }
    };
    screen->showOverlayBanner(bannerOptions);
}

void menuHandler::compassNorthMenu()
{
    static const CompassOption compassOptions[] = {
        {"Back", OptionsAction::Back},
        {"Dynamic", OptionsAction::Select, meshtastic_CompassMode_DYNAMIC},
        {"Fixed Ring", OptionsAction::Select, meshtastic_CompassMode_FIXED_RING},
        {"Freeze Heading", OptionsAction::Select, meshtastic_CompassMode_FREEZE_HEADING},
    };

    constexpr size_t compassCount = sizeof(compassOptions) / sizeof(compassOptions[0]);
    static std::array<const char *, compassCount> compassLabels{};

    auto bannerOptions = createStaticBannerOptions("North Directions?", compassOptions, compassLabels,
                                                   [](const CompassOption &option, int) -> void {
                                                       if (option.action == OptionsAction::Back) {
                                                           menuQueue = PositionBaseMenu;
                                                           screen->runNow();
                                                           return;
                                                       }

                                                       if (!option.hasValue) {
                                                           return;
                                                       }

                                                       if (uiconfig.compass_mode == option.value) {
                                                           return;
                                                       }

                                                       uiconfig.compass_mode = option.value;
                                                       saveUIConfig();
                                                       screen->setFrames(graphics::Screen::FOCUS_PRESERVE);
                                                   });

    int initialSelection = 0;
    for (size_t i = 0; i < compassCount; ++i) {
        if (compassOptions[i].hasValue && uiconfig.compass_mode == compassOptions[i].value) {
            initialSelection = static_cast<int>(i);
            break;
        }
    }
    bannerOptions.InitialSelected = initialSelection;

    screen->showOverlayBanner(bannerOptions);
}

#if !MESHTASTIC_EXCLUDE_GPS
void menuHandler::GPSToggleMenu()
{
    static const GPSToggleOption gpsToggleOptions[] = {
        {"Back", OptionsAction::Back},
        {"Enabled", OptionsAction::Select, meshtastic_Config_PositionConfig_GpsMode_ENABLED},
        {"Disabled", OptionsAction::Select, meshtastic_Config_PositionConfig_GpsMode_DISABLED},
    };

    constexpr size_t toggleCount = sizeof(gpsToggleOptions) / sizeof(gpsToggleOptions[0]);
    static std::array<const char *, toggleCount> toggleLabels{};

    auto bannerOptions =
        createStaticBannerOptions("Toggle GPS", gpsToggleOptions, toggleLabels, [](const GPSToggleOption &option, int) -> void {
            if (option.action == OptionsAction::Back) {
                menuQueue = PositionBaseMenu;
                screen->runNow();
                return;
            }

            if (!option.hasValue) {
                return;
            }

            if (config.position.gps_mode == option.value) {
                return;
            }

            config.position.gps_mode = option.value;
            if (option.value == meshtastic_Config_PositionConfig_GpsMode_ENABLED) {
                playGPSEnableBeep();
                gps->enable();
            } else {
                playGPSDisableBeep();
                gps->disable();
            }
            service->reloadConfig(SEGMENT_CONFIG);
        });

    int initialSelection = 0;
    for (size_t i = 0; i < toggleCount; ++i) {
        if (gpsToggleOptions[i].hasValue && config.position.gps_mode == gpsToggleOptions[i].value) {
            initialSelection = static_cast<int>(i);
            break;
        }
    }
    bannerOptions.InitialSelected = initialSelection;

    screen->showOverlayBanner(bannerOptions);
}
void menuHandler::GPSFormatMenu()
{
    static const GPSFormatOption formatOptionsHigh[] = {
        {"Back", OptionsAction::Back},
        {"Decimal Degrees", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_DEC},
        {"Degrees Minutes Seconds", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_DMS},
        {"Universal Transverse Mercator", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_UTM},
        {"Military Grid Reference System", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_MGRS},
        {"Open Location Code", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_OLC},
        {"Ordnance Survey Grid Ref", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_OSGR},
        {"Maidenhead Locator", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_MLS},
    };

    static const GPSFormatOption formatOptionsLow[] = {
        {"Back", OptionsAction::Back},
        {"DEC", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_DEC},
        {"DMS", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_DMS},
        {"UTM", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_UTM},
        {"MGRS", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_MGRS},
        {"OLC", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_OLC},
        {"OSGR", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_OSGR},
        {"MLS", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_MLS},
    };

    constexpr size_t formatCount = sizeof(formatOptionsHigh) / sizeof(formatOptionsHigh[0]);
    static std::array<const char *, formatCount> formatLabelsHigh{};
    static std::array<const char *, formatCount> formatLabelsLow{};

    auto onSelection = [](const GPSFormatOption &option, int) -> void {
        if (option.action == OptionsAction::Back) {
            menuQueue = PositionBaseMenu;
            screen->runNow();
            return;
        }

        if (!option.hasValue) {
            return;
        }

        if (uiconfig.gps_format == option.value) {
            return;
        }

        uiconfig.gps_format = option.value;
        saveUIConfig();
        service->reloadConfig(SEGMENT_CONFIG);
    };

    BannerOverlayOptions bannerOptions;
    int initialSelection = 0;

    if (currentResolution == ScreenResolution::High) {
        bannerOptions = createStaticBannerOptions("GPS Format", formatOptionsHigh, formatLabelsHigh, onSelection);
        for (size_t i = 0; i < formatCount; ++i) {
            if (formatOptionsHigh[i].hasValue && uiconfig.gps_format == formatOptionsHigh[i].value) {
                initialSelection = static_cast<int>(i);
                break;
            }
        }
    } else {
        bannerOptions = createStaticBannerOptions("GPS Format", formatOptionsLow, formatLabelsLow, onSelection);
        for (size_t i = 0; i < formatCount; ++i) {
            if (formatOptionsLow[i].hasValue && uiconfig.gps_format == formatOptionsLow[i].value) {
                initialSelection = static_cast<int>(i);
                break;
            }
        }
    }

    bannerOptions.InitialSelected = initialSelection;
    screen->showOverlayBanner(bannerOptions);
}

void menuHandler::GPSSmartPositionMenu()
{
    static const char *optionsArray[] = {"Back", "Enabled", "Disabled"};
    BannerOverlayOptions bannerOptions;
    bannerOptions.message = "Toggle Smart Position";
    if (currentResolution == ScreenResolution::UltraLow) {
        bannerOptions.message = "Smrt Postn";
    }
    bannerOptions.optionsArrayPtr = optionsArray;
    bannerOptions.optionsCount = 3;
    bannerOptions.bannerCallback = [](int selected) -> void {
        if (selected == 0) {
            menuQueue = PositionBaseMenu;
            screen->runNow();
        } else if (selected == 1) {
            config.position.position_broadcast_smart_enabled = true;
            saveUIConfig();
            service->reloadConfig(SEGMENT_CONFIG);
            rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000);
        } else if (selected == 2) {
            config.position.position_broadcast_smart_enabled = false;
            saveUIConfig();
            service->reloadConfig(SEGMENT_CONFIG);
            rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000);
        }
    };
    bannerOptions.InitialSelected = config.position.position_broadcast_smart_enabled ? 1 : 2;
    screen->showOverlayBanner(bannerOptions);
}

void menuHandler::GPSUpdateIntervalMenu()
{
    static const char *optionsArray[] = {"Back",      "8 seconds", "20 seconds", "40 seconds",  "1 minute",   "80 seconds",
                                         "2 minutes", "5 minutes", "10 minutes", "15 minutes",  "30 minutes", "1 hour",
                                         "6 hours",   "12 hours",  "24 hours",   "At Boot Only"};
    BannerOverlayOptions bannerOptions;
    bannerOptions.message = "Update Interval";
    bannerOptions.optionsArrayPtr = optionsArray;
    bannerOptions.optionsCount = 16;
    bannerOptions.bannerCallback = [](int selected) -> void {
        if (selected == 0) {
            menuQueue = PositionBaseMenu;
            screen->runNow();
        } else if (selected == 1) {
            config.position.gps_update_interval = 8;
        } else if (selected == 2) {
            config.position.gps_update_interval = 20;
        } else if (selected == 3) {
            config.position.gps_update_interval = 40;
        } else if (selected == 4) {
            config.position.gps_update_interval = 60;
        } else if (selected == 5) {
            config.position.gps_update_interval = 80;
        } else if (selected == 6) {
            config.position.gps_update_interval = 120;
        } else if (selected == 7) {
            config.position.gps_update_interval = 300;
        } else if (selected == 8) {
            config.position.gps_update_interval = 600;
        } else if (selected == 9) {
            config.position.gps_update_interval = 900;
        } else if (selected == 10) {
            config.position.gps_update_interval = 1800;
        } else if (selected == 11) {
            config.position.gps_update_interval = 3600;
        } else if (selected == 12) {
            config.position.gps_update_interval = 21600;
        } else if (selected == 13) {
            config.position.gps_update_interval = 43200;
        } else if (selected == 14) {
            config.position.gps_update_interval = 86400;
        } else if (selected == 15) {
            config.position.gps_update_interval = 2147483647; // At Boot Only
        }

        if (selected != 0) {
            saveUIConfig();
            service->reloadConfig(SEGMENT_CONFIG);
            rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000);
        }
    };

    if (config.position.gps_update_interval == 8) {
        bannerOptions.InitialSelected = 1;
    } else if (config.position.gps_update_interval == 20) {
        bannerOptions.InitialSelected = 2;
    } else if (config.position.gps_update_interval == 40) {
        bannerOptions.InitialSelected = 3;
    } else if (config.position.gps_update_interval == 60) {
        bannerOptions.InitialSelected = 4;
    } else if (config.position.gps_update_interval == 80) {
        bannerOptions.InitialSelected = 5;
    } else if (config.position.gps_update_interval == 120) {
        bannerOptions.InitialSelected = 6;
    } else if (config.position.gps_update_interval == 300) {
        bannerOptions.InitialSelected = 7;
    } else if (config.position.gps_update_interval == 600) {
        bannerOptions.InitialSelected = 8;
    } else if (config.position.gps_update_interval == 900) {
        bannerOptions.InitialSelected = 9;
    } else if (config.position.gps_update_interval == 1800) {
        bannerOptions.InitialSelected = 10;
    } else if (config.position.gps_update_interval == 3600) {
        bannerOptions.InitialSelected = 11;
    } else if (config.position.gps_update_interval == 21600) {
        bannerOptions.InitialSelected = 12;
    } else if (config.position.gps_update_interval == 43200) {
        bannerOptions.InitialSelected = 13;
    } else if (config.position.gps_update_interval == 86400) {
        bannerOptions.InitialSelected = 14;
    } else if (config.position.gps_update_interval == 2147483647) { // At Boot Only
        bannerOptions.InitialSelected = 15;
    } else {
        bannerOptions.InitialSelected = 0;
    }
    screen->showOverlayBanner(bannerOptions);
}

void menuHandler::GPSPositionBroadcastMenu()
{
    static const char *optionsArray[] = {"Back",     "1 minute", "90 seconds", "5 minutes", "15 minutes", "1 hour",
                                         "2 hours",  "3 hours",  "4 hours",    "5 hours",   "6 hours",    "12 hours",
                                         "18 hours", "24 hours", "36 hours",   "48 hours",  "72 hours"};
    BannerOverlayOptions bannerOptions;
    bannerOptions.message = "Broadcast Interval";
    bannerOptions.optionsArrayPtr = optionsArray;
    bannerOptions.optionsCount = 17;
    bannerOptions.bannerCallback = [](int selected) -> void {
        if (selected == 0) {
            menuQueue = PositionBaseMenu;
            screen->runNow();
        } else if (selected == 1) {
            config.position.position_broadcast_secs = 60;
        } else if (selected == 2) {
            config.position.position_broadcast_secs = 90;
        } else if (selected == 3) {
            config.position.position_broadcast_secs = 300;
        } else if (selected == 4) {
            config.position.position_broadcast_secs = 900;
        } else if (selected == 5) {
            config.position.position_broadcast_secs = 3600;
        } else if (selected == 6) {
            config.position.position_broadcast_secs = 7200;
        } else if (selected == 7) {
            config.position.position_broadcast_secs = 10800;
        } else if (selected == 8) {
            config.position.position_broadcast_secs = 14400;
        } else if (selected == 9) {
            config.position.position_broadcast_secs = 18000;
        } else if (selected == 10) {
            config.position.position_broadcast_secs = 21600;
        } else if (selected == 11) {
            config.position.position_broadcast_secs = 43200;
        } else if (selected == 12) {
            config.position.position_broadcast_secs = 64800;
        } else if (selected == 13) {
            config.position.position_broadcast_secs = 86400;
        } else if (selected == 14) {
            config.position.position_broadcast_secs = 129600;
        } else if (selected == 15) {
            config.position.position_broadcast_secs = 172800;
        } else if (selected == 16) {
            config.position.position_broadcast_secs = 259200;
        }

        if (selected != 0) {
            saveUIConfig();
            service->reloadConfig(SEGMENT_CONFIG);
            rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000);
        }
    };

    if (config.position.position_broadcast_secs == 60) {
        bannerOptions.InitialSelected = 1;
    } else if (config.position.position_broadcast_secs == 90) {
        bannerOptions.InitialSelected = 2;
    } else if (config.position.position_broadcast_secs == 300) {
        bannerOptions.InitialSelected = 3;
    } else if (config.position.position_broadcast_secs == 900) {
        bannerOptions.InitialSelected = 4;
    } else if (config.position.position_broadcast_secs == 3600) {
        bannerOptions.InitialSelected = 5;
    } else if (config.position.position_broadcast_secs == 7200) {
        bannerOptions.InitialSelected = 6;
    } else if (config.position.position_broadcast_secs == 10800) {
        bannerOptions.InitialSelected = 7;
    } else if (config.position.position_broadcast_secs == 14400) {
        bannerOptions.InitialSelected = 8;
    } else if (config.position.position_broadcast_secs == 18000) {
        bannerOptions.InitialSelected = 9;
    } else if (config.position.position_broadcast_secs == 21600) {
        bannerOptions.InitialSelected = 10;
    } else if (config.position.position_broadcast_secs == 43200) {
        bannerOptions.InitialSelected = 11;
    } else if (config.position.position_broadcast_secs == 64800) {
        bannerOptions.InitialSelected = 12;
    } else if (config.position.position_broadcast_secs == 86400) {
        bannerOptions.InitialSelected = 13;
    } else if (config.position.position_broadcast_secs == 129600) {
        bannerOptions.InitialSelected = 14;
    } else if (config.position.position_broadcast_secs == 172800) {
        bannerOptions.InitialSelected = 15;
    } else if (config.position.position_broadcast_secs == 259200) {
        bannerOptions.InitialSelected = 16;
    } else {
        bannerOptions.InitialSelected = 0;
    }
    screen->showOverlayBanner(bannerOptions);
}

#endif

void menuHandler::bluetoothToggleMenu()
{
    static const char *optionsArray[] = {"Back", "Enabled", "Disabled"};
    BannerOverlayOptions bannerOptions;
    bannerOptions.message = "Toggle Bluetooth";
    if (currentResolution == ScreenResolution::UltraLow) {
        bannerOptions.message = "Bluetooth";
    }
    bannerOptions.optionsArrayPtr = optionsArray;
    bannerOptions.optionsCount = 3;
    bannerOptions.bannerCallback = [](int selected) -> void {
        if (selected == 0)
            return;
        else if (selected != (config.bluetooth.enabled ? 1 : 2)) {
            InputEvent event = {.inputEvent = (input_broker_event)170, .kbchar = 170, .touchX = 0, .touchY = 0};
            inputBroker->injectInputEvent(&event);
        }
    };
    bannerOptions.InitialSelected = config.bluetooth.enabled ? 1 : 2;
    screen->showOverlayBanner(bannerOptions);
}

void menuHandler::BuzzerModeMenu()
{
    static const char *optionsArray[] = {"All Enabled", "All Disabled", "Notifications", "System Only", "DMs Only"};
    BannerOverlayOptions bannerOptions;
    bannerOptions.message = "Notification Sounds";
    bannerOptions.optionsArrayPtr = optionsArray;
    bannerOptions.optionsCount = 5;
    bannerOptions.bannerCallback = [](int selected) -> void {
        config.device.buzzer_mode = (meshtastic_Config_DeviceConfig_BuzzerMode)selected;
        service->reloadConfig(SEGMENT_CONFIG);
    };
    bannerOptions.InitialSelected = config.device.buzzer_mode;
    screen->showOverlayBanner(bannerOptions);
}

void menuHandler::BrightnessPickerMenu()
{
    static const char *optionsArray[] = {"Back", "Low", "Medium", "High"};

    // Get current brightness level to set initial selection
    int currentSelection = 1; // Default to Medium
    if (uiconfig.screen_brightness >= 255) {
        currentSelection = 3; // Very High
    } else if (uiconfig.screen_brightness >= 128) {
        currentSelection = 2; // High
    } else {
        currentSelection = 1; // Medium
    }

    BannerOverlayOptions bannerOptions;
    bannerOptions.message = "Brightness";
    bannerOptions.optionsArrayPtr = optionsArray;
    bannerOptions.optionsCount = 4;
    bannerOptions.bannerCallback = [](int selected) -> void {
        if (selected == 1) { // Medium
            uiconfig.screen_brightness = 64;
        } else if (selected == 2) { // High
            uiconfig.screen_brightness = 128;
        } else if (selected == 3) { // Very High
            uiconfig.screen_brightness = 255;
        }

        if (selected != 0) { // Not "Back"
                             // Apply brightness immediately
#if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190)
            // For HELTEC devices, use analogWrite to control backlight
            analogWrite(VTFT_LEDA, uiconfig.screen_brightness);
#elif defined(ST7789_CS) || defined(ST7796_CS)
            static_cast<TFTDisplay *>(screen->getDisplayDevice())->setDisplayBrightness(uiconfig.screen_brightness);
#elif defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107)
            screen->getDisplayDevice()->setBrightness(uiconfig.screen_brightness);
#endif

            // Save to device
            saveUIConfig();

            LOG_INFO("Screen brightness set to %d", uiconfig.screen_brightness);
        }
    };
    bannerOptions.InitialSelected = currentSelection;
    screen->showOverlayBanner(bannerOptions);
}

void menuHandler::switchToMUIMenu()
{
    static const char *optionsArray[] = {"No", "Yes"};
    BannerOverlayOptions bannerOptions;
    bannerOptions.message = "Switch to MUI?";
    bannerOptions.optionsArrayPtr = optionsArray;
    bannerOptions.optionsCount = 2;
    bannerOptions.bannerCallback = [](int selected) -> void {
        if (selected == 1) {
            config.display.displaymode = meshtastic_Config_DisplayConfig_DisplayMode_COLOR;
            config.bluetooth.enabled = false;
            service->reloadConfig(SEGMENT_CONFIG);
            rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000);
        }
    };
    screen->showOverlayBanner(bannerOptions);
}

void menuHandler::rebootMenu()
{
    static const char *optionsArray[] = {"Back", "Confirm"};
    BannerOverlayOptions bannerOptions;
    bannerOptions.message = "Reboot Device?";
    if (currentResolution == ScreenResolution::UltraLow) {
        bannerOptions.message = "Reboot";
    }
    bannerOptions.optionsArrayPtr = optionsArray;
    bannerOptions.optionsCount = 2;
    bannerOptions.bannerCallback = [](int selected) -> void {
        if (selected == 1) {
            IF_SCREEN(screen->showSimpleBanner("Rebooting...", 0));
            nodeDB->saveToDisk();
            messageStore.saveToFlash();
            rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000;
        } else {
            menuQueue = PowerMenu;
            screen->runNow();
        }
    };
    screen->showOverlayBanner(bannerOptions);
}

void menuHandler::shutdownMenu()
{
    static const char *optionsArray[] = {"Back", "Confirm"};
    BannerOverlayOptions bannerOptions;
    bannerOptions.message = "Shutdown Device?";
    if (currentResolution == ScreenResolution::UltraLow) {
        bannerOptions.message = "Shutdown";
    }
    bannerOptions.optionsArrayPtr = optionsArray;
    bannerOptions.optionsCount = 2;
    bannerOptions.bannerCallback = [](int selected) -> void {
        if (selected == 1) {
            InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_SHUTDOWN, .kbchar = 0, .touchX = 0, .touchY = 0};
            inputBroker->injectInputEvent(&event);
        } else {
            menuQueue = PowerMenu;
            screen->runNow();
        }
    };
    screen->showOverlayBanner(bannerOptions);
}

void menuHandler::removeFavoriteMenu()
{

    static const char *optionsArray[] = {"Back", "Yes"};
    BannerOverlayOptions bannerOptions;
    std::string message = "Unfavorite This Node?\n";
    const auto *node = nodeDB->getMeshNode(graphics::UIRenderer::currentFavoriteNodeNum);
    if (nodeInfoLiteHasUser(node)) {
        message += sanitizeString(node->long_name).substr(0, 15);
    }
    bannerOptions.message = message.c_str();
    bannerOptions.optionsArrayPtr = optionsArray;
    bannerOptions.optionsCount = 2;
    bannerOptions.bannerCallback = [](int selected) -> void {
        if (selected == 1) {
            LOG_INFO("Removing %x as favorite node", graphics::UIRenderer::currentFavoriteNodeNum);
            nodeDB->set_favorite(false, graphics::UIRenderer::currentFavoriteNodeNum);
            screen->setFrames(graphics::Screen::FOCUS_DEFAULT);
        }
    };
    screen->showOverlayBanner(bannerOptions);
}

void menuHandler::traceRouteMenu()
{
    screen->showNodePicker("Node to Trace", 30000, [](uint32_t nodenum) -> void {
        LOG_INFO("Menu: Node picker selected node 0x%08x, traceRouteModule=%p", nodenum, traceRouteModule);
        if (traceRouteModule) {
            traceRouteModule->startTraceRoute(nodenum);
        }
    });
}

void menuHandler::testMenu()
{

    enum optionsNumbers { Back, NumberPicker, ShowChirpy, TestAnnounce };
    static const char *optionsArray[5] = {"Back"};
    static int optionsEnumArray[5] = {Back};
    int options = 1;

    optionsArray[options] = "Number Picker";
    optionsEnumArray[options++] = NumberPicker;

    optionsArray[options] = screen->isFrameHidden("chirpy") ? "Show Chirpy" : "Hide Chirpy";
    optionsEnumArray[options++] = ShowChirpy;
#ifdef HAS_I2S
    optionsArray[options] = "Test Announce";
    optionsEnumArray[options++] = TestAnnounce;
#endif

    BannerOverlayOptions bannerOptions;
    bannerOptions.message = "Hidden Test Menu";
    bannerOptions.optionsArrayPtr = optionsArray;
    bannerOptions.optionsCount = options;
    bannerOptions.optionsEnumPtr = optionsEnumArray;
    bannerOptions.bannerCallback = [](int selected) -> void {
        if (selected == NumberPicker) {
            menuQueue = NumberTest;
            screen->runNow();
        } else if (selected == ShowChirpy) {
            screen->toggleFrameVisibility("chirpy");
            screen->setFrames(Screen::FOCUS_SYSTEM);

        } else if (selected == TestAnnounce) {
#ifdef HAS_I2S
            audioThread->readAloud("This is a test of the emergency broadcast system. This is only a test.");
#endif
        } else {
            menuQueue = SystemBaseMenu;
            screen->runNow();
        }
    };
    screen->showOverlayBanner(bannerOptions);
}

void menuHandler::numberTest()
{
    screen->showNumberPicker("Pick a number\n ", 30000, 4,
                             [](int number_picked) -> void { LOG_WARN("Nodenum: %u", number_picked); });
}

void menuHandler::wifiBaseMenu()
{
    enum optionsNumbers { Back, Wifi_toggle };

    static const char *optionsArray[] = {"Back", "WiFi Toggle"};
    BannerOverlayOptions bannerOptions;
    bannerOptions.message = "WiFi Menu";
    bannerOptions.optionsArrayPtr = optionsArray;
    bannerOptions.optionsCount = 2;
    bannerOptions.bannerCallback = [](int selected) -> void {
        if (selected == Wifi_toggle) {
            menuQueue = WifiToggleMenu;
            screen->runNow();
        }
    };
    screen->showOverlayBanner(bannerOptions);
}

void menuHandler::wifiToggleMenu()
{
    enum optionsNumbers { Back, Wifi_disable, Wifi_enable };

    static const char *optionsArray[] = {"Back", "WiFi Disabled", "WiFi Enabled"};
    BannerOverlayOptions bannerOptions;
    bannerOptions.message = "WiFi Actions";
    bannerOptions.optionsArrayPtr = optionsArray;
    bannerOptions.optionsCount = 3;
    if (config.network.wifi_enabled == true)
        bannerOptions.InitialSelected = 2;
    else
        bannerOptions.InitialSelected = 1;
    bannerOptions.bannerCallback = [](int selected) -> void {
        if (selected == Wifi_disable) {
            config.network.wifi_enabled = false;
            config.bluetooth.enabled = true;
            service->reloadConfig(SEGMENT_CONFIG);
            rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000);
        } else if (selected == Wifi_enable) {
            config.network.wifi_enabled = true;
            config.bluetooth.enabled = false;
            service->reloadConfig(SEGMENT_CONFIG);
            rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000);
        }
    };
    screen->showOverlayBanner(bannerOptions);
}

void menuHandler::screenOptionsMenu()
{
    // Check if brightness is supported
#if defined(T_DECK)
    // TDeck Doesn't seem to support brightness at all, at least not reliably
    bool hasSupportBrightness = false;
#elif defined(ST7789_CS) || defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107)
    bool hasSupportBrightness = true;
#else
    bool hasSupportBrightness = false;
#endif

    enum optionsNumbers { Back, Brightness, FrameToggles, DisplayUnits, MessageBubbles, Theme, LangPicker };
    static const char *optionsArray[8] = {"Back"};
    static int optionsEnumArray[8] = {Back};
    int options = 1;

    // Only show brightness for B&W displays
    if (hasSupportBrightness) {
        optionsArray[options] = "Brightness";
        optionsEnumArray[options++] = Brightness;
    }

    optionsArray[options] = "Frame Visibility";
    optionsEnumArray[options++] = FrameToggles;

    optionsArray[options] = "Display Units";
    optionsEnumArray[options++] = DisplayUnits;

    optionsArray[options] = "Message Bubbles";
    optionsEnumArray[options++] = MessageBubbles;

#if GRAPHICS_TFT_COLORING_ENABLED
    optionsArray[options] = "Theme";
    optionsEnumArray[options++] = Theme;
#endif

#if defined(ST7735_CS) || defined(ST7789_CS) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) ||      \
    defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS) || defined(RAK14014)
    optionsArray[options] = "Language";
    optionsEnumArray[options++] = LangPicker;
#endif

    BannerOverlayOptions bannerOptions;
    bannerOptions.message = "Display Options";
    bannerOptions.optionsArrayPtr = optionsArray;
    bannerOptions.optionsCount = options;
    bannerOptions.optionsEnumPtr = optionsEnumArray;
    bannerOptions.bannerCallback = [](int selected) -> void {
        if (selected == Brightness) {
            menuHandler::menuQueue = menuHandler::BrightnessPicker;
            screen->runNow();
        } else if (selected == FrameToggles) {
            menuHandler::menuQueue = menuHandler::FrameToggles;
            screen->runNow();
        } else if (selected == DisplayUnits) {
            menuHandler::menuQueue = menuHandler::DisplayUnits;
            screen->runNow();
        } else if (selected == MessageBubbles) {
            menuHandler::menuQueue = menuHandler::MessageBubblesMenu;
            screen->runNow();
        } else if (selected == Theme) {
            menuHandler::menuQueue = menuHandler::ThemeMenu;
            screen->runNow();
        } else if (selected == LangPicker) {
            menuHandler::menuQueue = menuHandler::LanguagePicker;
            screen->runNow();
        } else {
            menuQueue = SystemBaseMenu;
            screen->runNow();
        }
    };
    screen->showOverlayBanner(bannerOptions);
}

void menuHandler::powerMenu()
{

    enum optionsNumbers { Back, Reboot, Shutdown, MUI };
    static const char *optionsArray[4] = {"Back"};
    static int optionsEnumArray[4] = {Back};
    int options = 1;

    optionsArray[options] = "Reboot";
    optionsEnumArray[options++] = Reboot;

    optionsArray[options] = "Shutdown";
    optionsEnumArray[options++] = Shutdown;

#if HAS_TFT
    optionsArray[options] = "Switch to MUI";
    optionsEnumArray[options++] = MUI;
#endif

    BannerOverlayOptions bannerOptions;
    bannerOptions.message = "Reboot / Shutdown";
    if (currentResolution == ScreenResolution::UltraLow) {
        bannerOptions.message = "Power";
    }
    bannerOptions.optionsArrayPtr = optionsArray;
    bannerOptions.optionsCount = options;
    bannerOptions.optionsEnumPtr = optionsEnumArray;
    bannerOptions.bannerCallback = [](int selected) -> void {
        if (selected == Reboot) {
            menuHandler::menuQueue = menuHandler::RebootMenu;
            screen->runNow();
        } else if (selected == Shutdown) {
            menuHandler::menuQueue = menuHandler::ShutdownMenu;
            screen->runNow();
        } else if (selected == MUI) {
            menuHandler::menuQueue = menuHandler::MuiPicker;
            screen->runNow();
        } else {
            menuQueue = SystemBaseMenu;
            screen->runNow();
        }
    };
    screen->showOverlayBanner(bannerOptions);
}

void menuHandler::keyVerificationInitMenu()
{
    screen->showNodePicker("Node to Verify", 30000,
                           [](uint32_t selected) -> void { keyVerificationModule->sendInitialRequest(selected); });
}

void menuHandler::keyVerificationFinalPrompt()
{
    char message[40] = {0};
    memset(message, 0, sizeof(message));
    sprintf(message, "Verification: \n");
    keyVerificationModule->generateVerificationCode(message + 15); // send the toPhone packet

    if (screen) {
        static const char *optionsArray[] = {"Reject", "Accept"};
        graphics::BannerOverlayOptions options;
        options.message = message;
        options.durationMs = 30000;
        options.optionsArrayPtr = optionsArray;
        options.optionsCount = 2;
        options.notificationType = graphics::notificationTypeEnum::selection_picker;
        options.bannerCallback = [=](int selected) {
            if (selected == 1) {
                auto remoteNodePtr = nodeDB->getMeshNode(keyVerificationModule->getCurrentRemoteNode());
                remoteNodePtr->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK;
            }
        };
        screen->showOverlayBanner(options);
    }
}

void menuHandler::frameTogglesMenu()
{
    enum optionsNumbers {
        Finish,
        nodelist_nodes,
        nodelist_location,
        nodelist_lastheard,
        nodelist_hopsignal,
        nodelist_distance,
        nodelist_bearings,
        gps_position,
        lora,
        clock,
        show_favorites,
        show_env_telemetry,
        show_aq_telemetry,
        show_power,
        enumEnd
    };
    static const char *optionsArray[enumEnd] = {"Finish"};
    static int optionsEnumArray[enumEnd] = {Finish};
    int options = 1;

    // Track last selected index (not enum value!)
    static int lastSelectedIndex = 0;

#ifndef USE_EINK
    optionsArray[options] = screen->isFrameHidden("nodelist_nodes") ? "Show Node Lists" : "Hide Node Lists";
    optionsEnumArray[options++] = nodelist_nodes;
#else
    optionsArray[options] = screen->isFrameHidden("nodelist_lastheard") ? "Show NL - Last Heard" : "Hide NL - Last Heard";
    optionsEnumArray[options++] = nodelist_lastheard;
    optionsArray[options] = screen->isFrameHidden("nodelist_hopsignal") ? "Show NL - Hops/Signal" : "Hide NL - Hops/Signal";
    optionsEnumArray[options++] = nodelist_hopsignal;
#endif

#if HAS_GPS
#ifndef USE_EINK
    optionsArray[options] = screen->isFrameHidden("nodelist_location") ? "Show Position Lists" : "Hide Position Lists";
    optionsEnumArray[options++] = nodelist_location;
#else
    optionsArray[options] = screen->isFrameHidden("nodelist_distance") ? "Show NL - Distance" : "Hide NL - Distance";
    optionsEnumArray[options++] = nodelist_distance;
    optionsArray[options] = screen->isFrameHidden("nodelist_bearings") ? "Show NL - Bearings" : "Hide NL - Bearings";
    optionsEnumArray[options++] = nodelist_bearings;
#endif

    optionsArray[options] = screen->isFrameHidden("gps") ? "Show Position" : "Hide Position";
    optionsEnumArray[options++] = gps_position;
#endif

    optionsArray[options] = screen->isFrameHidden("lora") ? "Show LoRa" : "Hide LoRa";
    optionsEnumArray[options++] = lora;

    optionsArray[options] = screen->isFrameHidden("clock") ? "Show Clock" : "Hide Clock";
    optionsEnumArray[options++] = clock;

    optionsArray[options] = screen->isFrameHidden("show_favorites") ? "Show Favorites" : "Hide Favorites";
    optionsEnumArray[options++] = show_favorites;

    optionsArray[options] = moduleConfig.telemetry.environment_screen_enabled ? "Hide Env. Telemetry" : "Show Env. Telemetry";
    optionsEnumArray[options++] = show_env_telemetry;

    optionsArray[options] = moduleConfig.telemetry.air_quality_screen_enabled ? "Hide AQ Telemetry" : "Show AQ Telemetry";
    optionsEnumArray[options++] = show_aq_telemetry;

    optionsArray[options] = moduleConfig.telemetry.power_screen_enabled ? "Hide Power" : "Show Power";
    optionsEnumArray[options++] = show_power;

    BannerOverlayOptions bannerOptions;
    bannerOptions.message = "Show/Hide Frames";
    bannerOptions.optionsArrayPtr = optionsArray;
    bannerOptions.optionsCount = options;
    bannerOptions.optionsEnumPtr = optionsEnumArray;
    bannerOptions.InitialSelected = lastSelectedIndex; // Use index, not enum value

    bannerOptions.bannerCallback = [options](int selected) mutable -> void {
        // Find the index of selected in optionsEnumArray
        int idx = 0;
        for (; idx < options; ++idx) {
            if (optionsEnumArray[idx] == selected)
                break;
        }
        lastSelectedIndex = idx;

        if (selected == Finish) {
            screen->setFrames(Screen::FOCUS_DEFAULT);
        } else if (selected == nodelist_nodes) {
            screen->toggleFrameVisibility("nodelist_nodes");
            menuHandler::menuQueue = menuHandler::FrameToggles;
            screen->runNow();
        } else if (selected == nodelist_location) {
            screen->toggleFrameVisibility("nodelist_location");
            menuHandler::menuQueue = menuHandler::FrameToggles;
            screen->runNow();
        } else if (selected == nodelist_lastheard) {
            screen->toggleFrameVisibility("nodelist_lastheard");
            menuHandler::menuQueue = menuHandler::FrameToggles;
            screen->runNow();
        } else if (selected == nodelist_hopsignal) {
            screen->toggleFrameVisibility("nodelist_hopsignal");
            menuHandler::menuQueue = menuHandler::FrameToggles;
            screen->runNow();
        } else if (selected == nodelist_distance) {
            screen->toggleFrameVisibility("nodelist_distance");
            menuHandler::menuQueue = menuHandler::FrameToggles;
            screen->runNow();
        } else if (selected == nodelist_bearings) {
            screen->toggleFrameVisibility("nodelist_bearings");
            menuHandler::menuQueue = menuHandler::FrameToggles;
            screen->runNow();
        } else if (selected == gps_position) {
            screen->toggleFrameVisibility("gps");
            menuHandler::menuQueue = menuHandler::FrameToggles;
            screen->runNow();
        } else if (selected == lora) {
            screen->toggleFrameVisibility("lora");
            menuHandler::menuQueue = menuHandler::FrameToggles;
            screen->runNow();
        } else if (selected == clock) {
            screen->toggleFrameVisibility("clock");
            menuHandler::menuQueue = menuHandler::FrameToggles;
            screen->runNow();
        } else if (selected == show_favorites) {
            screen->toggleFrameVisibility("show_favorites");
            menuHandler::menuQueue = menuHandler::FrameToggles;
            screen->runNow();
        } else if (selected == show_env_telemetry) {
            moduleConfig.telemetry.environment_screen_enabled = !moduleConfig.telemetry.environment_screen_enabled;
            menuHandler::menuQueue = menuHandler::FrameToggles;
            screen->runNow();
        } else if (selected == show_aq_telemetry) {
            moduleConfig.telemetry.air_quality_screen_enabled = !moduleConfig.telemetry.air_quality_screen_enabled;
            menuHandler::menuQueue = menuHandler::FrameToggles;
            screen->runNow();
        } else if (selected == show_power) {
            moduleConfig.telemetry.power_screen_enabled = !moduleConfig.telemetry.power_screen_enabled;
            menuHandler::menuQueue = menuHandler::FrameToggles;
            screen->runNow();
        }
    };
    screen->showOverlayBanner(bannerOptions);
}

void menuHandler::displayUnitsMenu()
{
    enum optionsNumbers { Back, MetricUnits, ImperialUnits };

    static const char *optionsArray[] = {"Back", "Metric", "Imperial"};
    BannerOverlayOptions bannerOptions;
    bannerOptions.message = " Select display units";
    bannerOptions.optionsArrayPtr = optionsArray;
    bannerOptions.optionsCount = 3;
    if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL)
        bannerOptions.InitialSelected = 2;
    else
        bannerOptions.InitialSelected = 1;
    bannerOptions.bannerCallback = [](int selected) -> void {
        if (selected == MetricUnits) {
            config.display.units = meshtastic_Config_DisplayConfig_DisplayUnits_METRIC;
            service->reloadConfig(SEGMENT_CONFIG);
        } else if (selected == ImperialUnits) {
            config.display.units = meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL;
            service->reloadConfig(SEGMENT_CONFIG);
        } else {
            menuHandler::menuQueue = menuHandler::ScreenOptionsMenu;
            screen->runNow();
        }
    };
    screen->showOverlayBanner(bannerOptions);
}

void menuHandler::messageBubblesMenu()
{
    enum optionsNumbers { Back, ShowBubbles, HideBubbles };

    static const char *optionsArray[] = {"Back", "Show Bubbles", "Hide Bubbles"};
    BannerOverlayOptions bannerOptions;
    bannerOptions.message = "Message Bubbles";
    bannerOptions.optionsArrayPtr = optionsArray;
    bannerOptions.optionsCount = 3;
    bannerOptions.InitialSelected = config.display.enable_message_bubbles ? 1 : 2;
    bannerOptions.bannerCallback = [](int selected) -> void {
        if (selected == ShowBubbles) {
            config.display.enable_message_bubbles = true;
            service->reloadConfig(SEGMENT_CONFIG);
            LOG_INFO("Message bubbles enabled");
        } else if (selected == HideBubbles) {
            config.display.enable_message_bubbles = false;
            service->reloadConfig(SEGMENT_CONFIG);
            LOG_INFO("Message bubbles disabled");
        } else {
            menuHandler::menuQueue = menuHandler::ScreenOptionsMenu;
            screen->runNow();
        }
    };
    screen->showOverlayBanner(bannerOptions);
}

void menuHandler::themeMenu()
{
    // Build menu dynamically from the theme table.
    // Only visible themes appear!
    // Slot budget: 1 for "Back" + up to kMaxThemesInMenu visible themes.
    // Bump kMaxThemesInMenu if you add more themes than will fit here.
    constexpr size_t kMaxThemesInMenu = 15;
    const size_t visibleCount = getVisibleThemeCount();
    static const char *optionsArray[kMaxThemesInMenu + 1] = {"Back"};
    const size_t shownCount = (visibleCount < kMaxThemesInMenu) ? visibleCount : kMaxThemesInMenu;
    const int options = static_cast<int>(shownCount) + 1; // +1 for Back

    for (size_t i = 0; i < shownCount; i++) {
        optionsArray[i + 1] = getVisibleThemeByIndex(i).name;
    }

    BannerOverlayOptions bannerOptions;
    bannerOptions.message = "Theme";
    bannerOptions.optionsArrayPtr = optionsArray;
    bannerOptions.optionsCount = options;

    // Highlight the currently active theme (visible index + 1 for the Back
    // offset).  If the active theme is hidden, leave selection on "Back".
    const size_t activeVisible = getActiveVisibleThemeIndex();
    bannerOptions.InitialSelected = (activeVisible == SIZE_MAX) ? 0 : static_cast<int>(activeVisible) + 1;

    bannerOptions.bannerCallback = [](int selected) -> void {
        if (selected == 0) {
            // Back
            menuHandler::menuQueue = menuHandler::ScreenOptionsMenu;
            screen->runNow();
        } else {
            // Selection is an index into the VISIBLE themes (1-based, slot 0 is Back).
            const size_t visibleIdx = static_cast<size_t>(selected - 1);
            if (visibleIdx < getVisibleThemeCount()) {
                // Persist the theme's uniqueIdentifier so boot-time
                // resolveThemeIndex() can restore this theme on next startup.
                uiconfig.screen_rgb_color = COLOR565(255, 255, (getVisibleThemeByIndex(visibleIdx).uniqueIdentifier & 0x1F) << 3);
                loadThemeDefaults();
                saveUIConfig();
                screen->runNow();
            }
        }
    };
    screen->showOverlayBanner(bannerOptions);
}

void menuHandler::handleMenuSwitch(OLEDDisplay *display)
{
    if (menuQueue != MenuNone)
        test_count = 0;
    switch (menuQueue) {
    case MenuNone:
        break;
    case LoraMenu:
        loraMenu();
        break;
    case LoraPicker:
        LoraRegionPicker();
        break;
    case DeviceRolePicker:
        deviceRolePicker();
        break;
    case RadioPresetPicker:
        radioPresetPicker();
        break;
    case FrequencySlot:
        FrequencySlotPicker();
        break;
    case NoTimeoutLoraPicker:
        LoraRegionPicker(0);
        break;
    case TzPicker:
        TZPicker();
        break;
    case TwelveHourPicker:
        twelveHourPicker();
        break;
    case ClockFacePicker:
        clockFacePicker();
        break;
    case ClockMenu:
        clockMenu();
        break;
    case SystemBaseMenu:
        systemBaseMenu();
        break;
    case PositionBaseMenu:
        positionBaseMenu();
        break;
    case NodeBaseMenu:
        nodeListMenu();
        break;
#if !MESHTASTIC_EXCLUDE_GPS
    case GpsToggleMenu:
        GPSToggleMenu();
        break;
    case GpsFormatMenu:
        GPSFormatMenu();
        break;
    case GpsSmartPositionMenu:
        GPSSmartPositionMenu();
        break;
    case GpsUpdateIntervalMenu:
        GPSUpdateIntervalMenu();
        break;
    case GpsPositionBroadcastMenu:
        GPSPositionBroadcastMenu();
        break;
#endif
    case CompassPointNorthMenu:
        compassNorthMenu();
        break;
    case ResetNodeDbMenu:
        resetNodeDBMenu();
        break;
    case BuzzerModeMenuPicker:
        BuzzerModeMenu();
        break;
    case MuiPicker:
        switchToMUIMenu();
        break;
    case BrightnessPicker:
        BrightnessPickerMenu();
        break;
    case NodeNameLengthMenu:
        nodeNameLengthMenu();
        break;
    case RebootMenu:
        rebootMenu();
        break;
    case ShutdownMenu:
        shutdownMenu();
        break;
    case NodePickerMenu:
        NodePicker();
        break;
    case ManageNodeMenu:
        manageNodeMenu();
        break;
    case RemoveFavorite:
        removeFavoriteMenu();
        break;
    case TraceRouteMenu:
        traceRouteMenu();
        break;
    case TestMenu:
        testMenu();
        break;
    case NumberTest:
        numberTest();
        break;
    case WifiToggleMenu:
        wifiToggleMenu();
        break;
    case KeyVerificationInit:
        keyVerificationInitMenu();
        break;
    case KeyVerificationFinalPrompt:
        keyVerificationFinalPrompt();
        break;
    case BluetoothToggleMenu:
        bluetoothToggleMenu();
        break;
    case ScreenOptionsMenu:
        screenOptionsMenu();
        break;
    case PowerMenu:
        powerMenu();
        break;
    case FrameToggles:
        frameTogglesMenu();
        break;
    case DisplayUnits:
        displayUnitsMenu();
        break;
    case ThrottleMessage:
        screen->showSimpleBanner("Too Many Attempts\nTry again in 60 seconds.", 5000);
        break;
    case MessageResponseMenu:
        messageResponseMenu();
        break;
    case ReplyMenu:
        replyMenu();
        break;
    case DeleteMessagesMenu:
        deleteMessagesMenu();
        break;
    case MessageViewModeMenu:
        messageViewModeMenu();
        break;
    case MessageBubblesMenu:
        messageBubblesMenu();
        break;
    case ThemeMenu:
        themeMenu();
        break;
    case LanguagePicker:
        languagePickerMenu();
        break;
    case HamModeConfirm:
        hamModeConfirmMenu();
        break;
    case LicensedToNormalConfirm:
        licensedToNormalConfirmMenu();
        break;
    }
    menuQueue = MenuNone;
}

void menuHandler::languagePickerMenu()
{
    enum optionsNumbers { Back, English, Chinese };
    static const char *optionsArray[3] = {"Back"};
    static int optionsEnumArray[3] = {Back};
    int options = 1;

    optionsArray[options] = "English";
    optionsEnumArray[options++] = English;

    optionsArray[options] = "Chinese";
    optionsEnumArray[options++] = Chinese;

    BannerOverlayOptions bannerOptions;
    bannerOptions.message = "Language";
    bannerOptions.optionsArrayPtr = optionsArray;
    bannerOptions.optionsCount = options;
    bannerOptions.optionsEnumPtr = optionsEnumArray;
    bannerOptions.bannerCallback = [](int selected) -> void {
        if (selected == English) {
            uiconfig.language = meshtastic_Language_ENGLISH;
            meshtastic::l10n::currentDisplayLanguage = meshtastic_Language_ENGLISH;
            saveUIConfig();
        } else if (selected == Chinese) {
            uiconfig.language = meshtastic_Language_SIMPLIFIED_CHINESE;
            meshtastic::l10n::currentDisplayLanguage = meshtastic_Language_SIMPLIFIED_CHINESE;
            saveUIConfig();
#if defined(ST7735_CS) || defined(ST7789_CS) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) ||      \
    defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS) || defined(RAK14014)
            screen->loadCJKFontIfNeeded();
#endif
        }
        menuQueue = ScreenOptionsMenu;
        screen->runNow();
    };
    screen->showOverlayBanner(bannerOptions);
}

void menuHandler::saveUIConfig()
{
    nodeDB->saveProto("/prefs/uiconfig.proto", meshtastic_DeviceUIConfig_size, &meshtastic_DeviceUIConfig_msg, &uiconfig);
}

} // namespace graphics

#endif
