#pragma once

#include <helpers/ui/UIScreen.h>
#include <helpers/ui/DisplayDriver.h>
#include <helpers/ChannelDetails.h>
#include <MeshCore.h>
#include <Packet.h>
#include "EmojiSprites.h"

// SD card message persistence
#if defined(HAS_SDCARD) && defined(ESP32)
  #include <SD.h>
#endif

// Maximum messages to store in history
#define CHANNEL_MSG_HISTORY_SIZE 300
#define CHANNEL_MSG_TEXT_LEN 160
#define MSG_PATH_MAX 20  // Max repeater hops stored per message

#ifndef MAX_GROUP_CHANNELS
  #define MAX_GROUP_CHANNELS 20
#endif

// ---------------------------------------------------------------------------
// On-disk format for message persistence (SD card)
// ---------------------------------------------------------------------------
#define MSG_FILE_MAGIC   0x4D434853  // "MCHS" - MeshCore History Store
#define MSG_FILE_VERSION 3           // v3: MSG_PATH_MAX=20, reserved→snr field
#define MSG_FILE_PATH    "/meshcore/messages.bin"

struct __attribute__((packed)) MsgFileHeader {
  uint32_t magic;
  uint16_t version;
  uint16_t capacity;
  uint16_t count;
  int16_t  newestIdx;
  // 12 bytes total
};

struct __attribute__((packed)) MsgFileRecord {
  uint32_t timestamp;
  uint8_t  path_len;
  uint8_t  channel_idx;
  uint8_t  valid;
  int8_t   snr;          // Receive SNR × 4 (was reserved; 0 = unknown)
  uint8_t  path[MSG_PATH_MAX];  // Repeater hop hashes (first byte of pub key)
  char     text[CHANNEL_MSG_TEXT_LEN];
  // 188 bytes total
};

class UITask;  // Forward declaration
class MyMesh;  // Forward declaration
extern MyMesh the_mesh;

class ChannelScreen : public UIScreen {
public:
  struct ChannelMessage {
    uint32_t timestamp;
    uint8_t path_len;
    uint8_t channel_idx;  // Which channel this message belongs to
    int8_t  snr;          // Receive SNR × 4 (0 if locally sent or unknown)
    uint8_t path[MSG_PATH_MAX];  // Repeater hop hashes
    char text[CHANNEL_MSG_TEXT_LEN];
    bool valid;
  };

private:
  UITask* _task;
  mesh::RTCClock* _rtc;
  
  ChannelMessage _messages[CHANNEL_MSG_HISTORY_SIZE];
  int _msgCount;      // Total messages stored
  int _newestIdx;     // Index of newest message (circular buffer)
  int _scrollPos;     // Current scroll position (0 = newest)
  int _msgsPerPage;   // Messages that fit on screen
  uint8_t _viewChannelIdx;  // Which channel we're currently viewing
  bool _sdReady;      // SD card is available for persistence
  bool _showPathOverlay;  // Show path detail overlay for last received msg
  int _pathScrollPos;     // Scroll offset within path overlay hop list
  int _pathHopsVisible;   // Hops that fit on screen (set during render)

  // Reply select mode — press R to pick a message and reply with @mention
  bool _replySelectMode;       // True when user is picking a message to reply to
  int  _replySelectPos;        // Index into chronological channelMsgs[] (0=oldest)
  int  _replyChannelMsgCount;  // Cached count from last render (for input bounds)

  // Per-channel unread message counts (standalone mode)
  // Index 0..MAX_GROUP_CHANNELS-1 for channel messages
  // Index MAX_GROUP_CHANNELS for DMs (channel_idx == 0xFF)
  int _unread[MAX_GROUP_CHANNELS + 1];
  
public:
  ChannelScreen(UITask* task, mesh::RTCClock* rtc) 
    : _task(task), _rtc(rtc), _msgCount(0), _newestIdx(-1), _scrollPos(0), 
      _msgsPerPage(6), _viewChannelIdx(0), _sdReady(false), _showPathOverlay(false), _pathScrollPos(0), _pathHopsVisible(20),
      _replySelectMode(false), _replySelectPos(-1), _replyChannelMsgCount(0) {
    // Initialize all messages as invalid
    for (int i = 0; i < CHANNEL_MSG_HISTORY_SIZE; i++) {
      _messages[i].valid = false;
      memset(_messages[i].path, 0, MSG_PATH_MAX);
    }
    // Initialize unread counts
    memset(_unread, 0, sizeof(_unread));
  }

  void setSDReady(bool ready) { _sdReady = ready; }

  // Add a new message to the history
  void addMessage(uint8_t channel_idx, uint8_t path_len, const char* sender, const char* text,
                  const uint8_t* path_bytes = nullptr, int8_t snr = 0) {
    // Move to next slot in circular buffer
    _newestIdx = (_newestIdx + 1) % CHANNEL_MSG_HISTORY_SIZE;
    
    ChannelMessage* msg = &_messages[_newestIdx];
    msg->timestamp = _rtc->getCurrentTime();
    msg->path_len = path_len;
    msg->channel_idx = channel_idx;
    msg->snr = snr;
    msg->valid = true;
    
    // Store path hop hashes
    memset(msg->path, 0, MSG_PATH_MAX);
    if (path_bytes && path_len > 0 && path_len != 0xFF) {
      int n = mesh::Packet::getPathByteLenFor(path_len);
      if (n > MSG_PATH_MAX) n = MSG_PATH_MAX;
      memcpy(msg->path, path_bytes, n);
    }
    
    // Sanitize emoji: replace UTF-8 emoji sequences with single-byte escape codes
    // The text already contains "Sender: message" format
    emojiSanitize(text, msg->text, CHANNEL_MSG_TEXT_LEN);
    
    if (_msgCount < CHANNEL_MSG_HISTORY_SIZE) {
      _msgCount++;
    }
    
    // Reset scroll to show newest message
    _scrollPos = 0;
    _showPathOverlay = false;  // Dismiss overlay on new message
    _pathScrollPos = 0;
    _replySelectMode = false;  // Dismiss reply select on new message
    _replySelectPos = -1;

    // Track unread count for this channel (only for received messages, not sent)
    // path_len == 0 means locally sent
    if (path_len != 0) {
      int unreadSlot = (channel_idx == 0xFF) ? MAX_GROUP_CHANNELS : channel_idx;
      if (unreadSlot >= 0 && unreadSlot <= MAX_GROUP_CHANNELS) {
        _unread[unreadSlot]++;
      }
    }

    // Persist to SD card
    saveToSD();
  }

  // Get count of messages for the currently viewed channel
  int getMessageCountForChannel() const {
    int count = 0;
    for (int i = 0; i < CHANNEL_MSG_HISTORY_SIZE; i++) {
      if (_messages[i].valid && _messages[i].channel_idx == _viewChannelIdx) {
        count++;
      }
    }
    return count;
  }

  int getMessageCount() const { return _msgCount; }
  
  uint8_t getViewChannelIdx() const { return _viewChannelIdx; }
  void setViewChannelIdx(uint8_t idx) {
    _viewChannelIdx = idx;
    _scrollPos = 0;
    _showPathOverlay = false;
    _pathScrollPos = 0;
    markChannelRead(idx);
  }
  bool isShowingPathOverlay() const { return _showPathOverlay; }
  void dismissPathOverlay() { _showPathOverlay = false; _pathScrollPos = 0; }

  // --- Reply select mode (R key → pick a message → Enter to @mention reply) ---
  bool isReplySelectMode() const { return _replySelectMode; }
  void exitReplySelect() { _replySelectMode = false; _replySelectPos = -1; }

  // Extract sender name from a "Sender: message" formatted text.
  // Returns true if a sender was found, fills senderBuf (null-terminated).
  static bool extractSenderName(const char* msgText, char* senderBuf, int bufLen) {
    const char* colon = strstr(msgText, ": ");
    if (!colon || colon == msgText) return false;
    int nameLen = colon - msgText;
    if (nameLen >= bufLen) nameLen = bufLen - 1;
    memcpy(senderBuf, msgText, nameLen);
    senderBuf[nameLen] = '\0';
    return true;
  }

  // Get the sender name of the currently selected message in reply select mode.
  // Returns true and fills senderBuf if valid selection exists.
  bool getReplySelectSender(char* senderBuf, int bufLen) {
    if (!_replySelectMode || _replySelectPos < 0) return false;

    // Rebuild the channel message list (same logic as render)
    static int rsMsgs[CHANNEL_MSG_HISTORY_SIZE];
    int count = 0;
    for (int i = 0; i < _msgCount && count < CHANNEL_MSG_HISTORY_SIZE; i++) {
      int idx = _newestIdx - i;
      while (idx < 0) idx += CHANNEL_MSG_HISTORY_SIZE;
      idx = idx % CHANNEL_MSG_HISTORY_SIZE;
      if (_messages[idx].valid && _messages[idx].channel_idx == _viewChannelIdx) {
        rsMsgs[count++] = idx;
      }
    }
    // Reverse to chronological (oldest first)
    for (int l = 0, r = count - 1; l < r; l++, r--) {
      int t = rsMsgs[l]; rsMsgs[l] = rsMsgs[r]; rsMsgs[r] = t;
    }

    if (_replySelectPos >= count) return false;
    int idx = rsMsgs[_replySelectPos];
    return extractSenderName(_messages[idx].text, senderBuf, bufLen);
  }

  // Get the ChannelMessage pointer for the currently selected reply message.
  ChannelMessage* getReplySelectMsg() {
    if (!_replySelectMode || _replySelectPos < 0) return nullptr;

    static int rsMsgs[CHANNEL_MSG_HISTORY_SIZE];
    int count = 0;
    for (int i = 0; i < _msgCount && count < CHANNEL_MSG_HISTORY_SIZE; i++) {
      int idx = _newestIdx - i;
      while (idx < 0) idx += CHANNEL_MSG_HISTORY_SIZE;
      idx = idx % CHANNEL_MSG_HISTORY_SIZE;
      if (_messages[idx].valid && _messages[idx].channel_idx == _viewChannelIdx) {
        rsMsgs[count++] = idx;
      }
    }
    for (int l = 0, r = count - 1; l < r; l++, r--) {
      int t = rsMsgs[l]; rsMsgs[l] = rsMsgs[r]; rsMsgs[r] = t;
    }

    if (_replySelectPos >= count) return nullptr;
    return &_messages[rsMsgs[_replySelectPos]];
  }

  // --- Unread message tracking (standalone mode) ---

  // Mark all messages for a channel as read
  void markChannelRead(uint8_t channel_idx) {
    int slot = (channel_idx == 0xFF) ? MAX_GROUP_CHANNELS : channel_idx;
    if (slot >= 0 && slot <= MAX_GROUP_CHANNELS) {
      _unread[slot] = 0;
    }
  }

  // Get unread count for a specific channel
  int getUnreadForChannel(uint8_t channel_idx) const {
    int slot = (channel_idx == 0xFF) ? MAX_GROUP_CHANNELS : channel_idx;
    if (slot >= 0 && slot <= MAX_GROUP_CHANNELS) {
      return _unread[slot];
    }
    return 0;
  }

  // Get total unread across all channels
  int getTotalUnread() const {
    int total = 0;
    for (int i = 0; i <= MAX_GROUP_CHANNELS; i++) {
      total += _unread[i];
    }
    return total;
  }

  // Find the newest RECEIVED message for the current channel
  // (path_len != 0 means received, path_len 0 = locally sent)
  ChannelMessage* getNewestReceivedMsg() {
    for (int i = 0; i < _msgCount; i++) {
      int idx = _newestIdx - i;
      while (idx < 0) idx += CHANNEL_MSG_HISTORY_SIZE;
      idx = idx % CHANNEL_MSG_HISTORY_SIZE;
      if (_messages[idx].valid && _messages[idx].channel_idx == _viewChannelIdx
          && _messages[idx].path_len != 0) {
        return &_messages[idx];
      }
    }
    return nullptr;
  }

  // Format the path of the newest received message as paste-ready text
  // Output: comma-separated hex prefixes e.g. "30, 3b, 9b, 05, e8, 36"
  // Returns length written (0 if no path available)
  int formatPathAsText(char* buf, int bufLen) {
    ChannelMessage* msg = getNewestReceivedMsg();
    if (!msg || msg->path_len == 0 || msg->path_len == 0xFF) return 0;
    
    int pos = 0;
    uint8_t hopCount = msg->path_len & 63;
    uint8_t bytesPerHop = (msg->path_len >> 6) + 1;
    
    for (int h = 0; h < hopCount && pos < bufLen - 1; h++) {
      if (h > 0) pos += snprintf(buf + pos, bufLen - pos, ", ");
      int offset = h * bytesPerHop;
      for (int b = 0; b < bytesPerHop && pos < bufLen - 1; b++) {
        pos += snprintf(buf + pos, bufLen - pos, "%02x", msg->path[offset + b]);
      }
    }
    
    return pos;
  }

  // -----------------------------------------------------------------------
  // SD card persistence
  // -----------------------------------------------------------------------

  // Save the entire message buffer to SD card.
  // File: /meshcore/messages.bin  (~50 KB for 300 messages)
  void saveToSD() {
#if defined(HAS_SDCARD) && defined(ESP32)
    if (!_sdReady) return;

    // Ensure directory exists
    if (!SD.exists("/meshcore")) {
      SD.mkdir("/meshcore");
    }

    File f = SD.open(MSG_FILE_PATH, "w", true);
    if (!f) {
      Serial.println("ChannelScreen: SD save failed - can't open file");
      return;
    }

    // Write header
    MsgFileHeader hdr;
    hdr.magic    = MSG_FILE_MAGIC;
    hdr.version  = MSG_FILE_VERSION;
    hdr.capacity = CHANNEL_MSG_HISTORY_SIZE;
    hdr.count    = (uint16_t)_msgCount;
    hdr.newestIdx = (int16_t)_newestIdx;
    f.write((uint8_t*)&hdr, sizeof(hdr));

    // Write all message slots (including invalid ones - preserves circular buffer layout)
    for (int i = 0; i < CHANNEL_MSG_HISTORY_SIZE; i++) {
      MsgFileRecord rec;
      rec.timestamp   = _messages[i].timestamp;
      rec.path_len    = _messages[i].path_len;
      rec.channel_idx = _messages[i].channel_idx;
      rec.valid       = _messages[i].valid ? 1 : 0;
      rec.snr         = _messages[i].snr;
      memcpy(rec.path, _messages[i].path, MSG_PATH_MAX);
      memcpy(rec.text, _messages[i].text, CHANNEL_MSG_TEXT_LEN);
      f.write((uint8_t*)&rec, sizeof(rec));
    }

    f.close();
    digitalWrite(SDCARD_CS, HIGH);  // Release SD CS
#endif
  }

  // Load message buffer from SD card.  Returns true if messages were loaded.
  bool loadFromSD() {
#if defined(HAS_SDCARD) && defined(ESP32)
    if (!_sdReady) return false;

    if (!SD.exists(MSG_FILE_PATH)) {
      Serial.println("ChannelScreen: No saved messages on SD");
      return false;
    }

    File f = SD.open(MSG_FILE_PATH, "r");
    if (!f) {
      Serial.println("ChannelScreen: SD load failed - can't open file");
      return false;
    }

    // Read and validate header
    MsgFileHeader hdr;
    if (f.read((uint8_t*)&hdr, sizeof(hdr)) != sizeof(hdr)) {
      Serial.println("ChannelScreen: SD load failed - short header");
      f.close();
      return false;
    }

    if (hdr.magic != MSG_FILE_MAGIC) {
      Serial.printf("ChannelScreen: SD load failed - bad magic 0x%08X\n", hdr.magic);
      f.close();
      return false;
    }

    if (hdr.version != MSG_FILE_VERSION) {
      Serial.printf("ChannelScreen: SD load failed - version %d (expected %d)\n",
                    hdr.version, MSG_FILE_VERSION);
      f.close();
      return false;
    }

    if (hdr.capacity != CHANNEL_MSG_HISTORY_SIZE) {
      Serial.printf("ChannelScreen: SD load failed - capacity %d (expected %d)\n",
                    hdr.capacity, CHANNEL_MSG_HISTORY_SIZE);
      f.close();
      return false;
    }

    // Read message records
    int loaded = 0;
    for (int i = 0; i < CHANNEL_MSG_HISTORY_SIZE; i++) {
      MsgFileRecord rec;
      if (f.read((uint8_t*)&rec, sizeof(rec)) != sizeof(rec)) {
        Serial.printf("ChannelScreen: SD load - short read at record %d\n", i);
        break;
      }
      _messages[i].timestamp   = rec.timestamp;
      _messages[i].path_len    = rec.path_len;
      _messages[i].channel_idx = rec.channel_idx;
      _messages[i].valid       = (rec.valid != 0);
      _messages[i].snr         = rec.snr;
      memcpy(_messages[i].path, rec.path, MSG_PATH_MAX);
      memcpy(_messages[i].text, rec.text, CHANNEL_MSG_TEXT_LEN);
      if (_messages[i].valid) loaded++;
    }

    _msgCount   = (int)hdr.count;
    _newestIdx  = (int)hdr.newestIdx;
    _scrollPos  = 0;

    // Sanity-check restored state
    if (_newestIdx < -1 || _newestIdx >= CHANNEL_MSG_HISTORY_SIZE) _newestIdx = -1;
    if (_msgCount < 0 || _msgCount > CHANNEL_MSG_HISTORY_SIZE) _msgCount = loaded;

    f.close();
    digitalWrite(SDCARD_CS, HIGH);  // Release SD CS
    Serial.printf("ChannelScreen: Loaded %d messages from SD (count=%d, newest=%d)\n",
                  loaded, _msgCount, _newestIdx);
    return loaded > 0;
#else
    return false;
#endif
  }

  // -----------------------------------------------------------------------
  // Rendering
  // -----------------------------------------------------------------------

  int render(DisplayDriver& display) override {
    char tmp[40];
    
    // Header - show current channel name
    display.setCursor(0, 0);
    display.setTextSize(1);
    display.setColor(DisplayDriver::GREEN);
    
    // Get channel name
    ChannelDetails channel;
    if (the_mesh.getChannel(_viewChannelIdx, channel)) {
      display.print(channel.name);
    } else {
      sprintf(tmp, "Channel %d", _viewChannelIdx);
      display.print(tmp);
    }
    
    // Message count for this channel on right
    int channelMsgCount = getMessageCountForChannel();
    sprintf(tmp, "[%d]", channelMsgCount);
    display.setCursor(display.width() - display.getTextWidth(tmp) - 2, 0);
    display.print(tmp);
    
    // Divider line
    display.drawRect(0, 11, display.width(), 1);
    
    // --- Path detail overlay ---
    if (_showPathOverlay) {
      display.setTextSize(0);
      int lineH = 9;
      int y = 14;
      
      ChannelMessage* msg = getNewestReceivedMsg();
      if (!msg) {
        display.setCursor(0, y);
        display.setColor(DisplayDriver::LIGHT);
        display.print("No received messages");
      } else {
        // Message preview (first ~30 chars)
        display.setCursor(0, y);
        display.setColor(DisplayDriver::LIGHT);
        char preview[32];
        strncpy(preview, msg->text, 31);
        preview[31] = '\0';
        display.print(preview);
        y += lineH;
        
        // Age
        uint32_t age = _rtc->getCurrentTime() - msg->timestamp;
        display.setCursor(0, y);
        display.setColor(DisplayDriver::YELLOW);
        if (age < 60) sprintf(tmp, "Age: %ds", age);
        else if (age < 3600) sprintf(tmp, "Age: %dm", age / 60);
        else if (age < 86400) sprintf(tmp, "Age: %dh", age / 3600);
        else sprintf(tmp, "Age: %dd", age / 86400);
        display.print(tmp);
        y += lineH;
        
        // Route type
        display.setCursor(0, y);
        uint8_t plen = msg->path_len;
        uint8_t hopCount = plen & 63;           // extract hop count from encoded path_len
        uint8_t bytesPerHop = (plen >> 6) + 1;  // 1, 2, or 3 bytes per hop
        if (plen == 0xFF) {
          display.setColor(DisplayDriver::LIGHT);
          display.print("Route: Direct");
        } else if (plen == 0) {
          display.setColor(DisplayDriver::LIGHT);
          display.print("Route: Local/Sent");
        } else {
          display.setColor(DisplayDriver::GREEN);
          sprintf(tmp, "Route: %d hop%s (%dB)", hopCount, hopCount == 1 ? "" : "s", bytesPerHop);
          display.print(tmp);
        }
        y += lineH;

        // SNR (if available — value is SNR×4)
        if (msg->snr != 0) {
          display.setCursor(0, y);
          display.setColor(DisplayDriver::YELLOW);
          int snr_whole = msg->snr / 4;
          int snr_frac = ((abs(msg->snr) % 4) * 10) / 4;
          sprintf(tmp, "SNR: %d.%ddB", snr_whole, snr_frac);
          display.print(tmp);
          y += lineH;
        }
        y += 2;
        
        // Show each hop resolved against contacts (scrollable)
        if (hopCount > 0 && plen != 0xFF) {
          int displayHops = hopCount;
          int footerReserve = 26;  // footer + divider
          int scrollBarW = 4;
          int maxY = display.height() - footerReserve;
          int hopAreaTop = y;
          
          // Calculate how many hops fit in the visible area
          int hopsVisible = (maxY - hopAreaTop) / lineH;
          if (hopsVisible < 1) hopsVisible = 1;
          _pathHopsVisible = hopsVisible;  // Cache for input handler
          bool needsScroll = displayHops > hopsVisible;
          
          // Clamp scroll position
          int maxScroll = displayHops - hopsVisible;
          if (maxScroll < 0) maxScroll = 0;
          if (_pathScrollPos > maxScroll) _pathScrollPos = maxScroll;
          
          // Available text width (narrower if scroll bar present)
          int textRight = needsScroll ? display.width() - scrollBarW - 2 : display.width();
          (void)textRight;  // reserved for future truncation
          
          int startHop = _pathScrollPos;
          int endHop = startHop + hopsVisible;
          if (endHop > displayHops) endHop = displayHops;
          
          for (int h = startHop; h < endHop && y + lineH <= maxY; h++) {
            int hopOffset = h * bytesPerHop;  // byte offset into path[]
            display.setCursor(0, y);
            display.setColor(DisplayDriver::LIGHT);
            sprintf(tmp, " %d: ", h + 1);
            display.print(tmp);
            
            // Show hex prefix (1, 2, or 3 bytes)
            display.setColor(DisplayDriver::LIGHT);
            if (bytesPerHop == 1) {
              sprintf(tmp, "%02X ", msg->path[hopOffset]);
            } else if (bytesPerHop == 2) {
              sprintf(tmp, "%02X%02X ", msg->path[hopOffset], msg->path[hopOffset + 1]);
            } else {
              sprintf(tmp, "%02X%02X%02X ", msg->path[hopOffset], msg->path[hopOffset + 1], msg->path[hopOffset + 2]);
            }
            display.print(tmp);
            
            // Try to resolve name: prefer repeaters, then any contact
            bool resolved = false;
            int numContacts = the_mesh.getNumContacts();
            ContactInfo contact;
            char filteredName[32];
            
            // First pass: repeaters only
            for (uint32_t ci = 0; ci < numContacts && !resolved; ci++) {
              if (the_mesh.getContactByIdx(ci, contact)) {
                if (memcmp(contact.id.pub_key, &msg->path[hopOffset], bytesPerHop) == 0
                    && contact.type == ADV_TYPE_REPEATER) {
                  display.setColor(DisplayDriver::GREEN);
                  display.translateUTF8ToBlocks(filteredName, contact.name, sizeof(filteredName));
                  display.print(filteredName);
                  resolved = true;
                }
              }
            }
            // Second pass: any contact type
            if (!resolved) {
              for (uint32_t ci = 0; ci < numContacts; ci++) {
                if (the_mesh.getContactByIdx(ci, contact)) {
                  if (memcmp(contact.id.pub_key, &msg->path[hopOffset], bytesPerHop) == 0) {
                    display.setColor(DisplayDriver::YELLOW);
                    display.translateUTF8ToBlocks(filteredName, contact.name, sizeof(filteredName));
                    display.print(filteredName);
                    resolved = true;
                    break;
                  }
                }
              }
            }
            // No name resolved - hex prefix already shown, add "?" marker
            if (!resolved) {
              display.setColor(DisplayDriver::LIGHT);
              display.print("?");
            }
            y += lineH;
          }
          
          // Scroll bar (right edge) when hops exceed visible area
          if (needsScroll) {
            int sbX = display.width() - scrollBarW;
            int sbTop = hopAreaTop;
            int sbHeight = maxY - hopAreaTop;
            
            // Outline
            display.setColor(DisplayDriver::LIGHT);
            display.drawRect(sbX, sbTop, scrollBarW, sbHeight);
            
            // Proportional thumb
            int thumbH = (hopsVisible * sbHeight) / displayHops;
            if (thumbH < 4) thumbH = 4;
            int thumbY = sbTop + (_pathScrollPos * (sbHeight - thumbH)) / maxScroll;
            display.setColor(DisplayDriver::GREEN);
            for (int ty = thumbY + 1; ty < thumbY + thumbH - 1; ty++)
              display.drawRect(sbX + 1, ty, scrollBarW - 2, 1);
          }
        }
      }
      
      // Overlay footer
      display.setTextSize(1);
      int footerY = display.height() - 12;
      display.drawRect(0, footerY - 2, display.width(), 1);
      display.setCursor(0, footerY);
      display.setColor(DisplayDriver::YELLOW);
#if defined(LilyGo_T5S3_EPaper_Pro)
      display.print("Back");
      const char* copyHint = "Tap:Dismiss";
#else
      display.print("Q:Back");
      // Show scroll hint if path is scrollable
      if (msg && (msg->path_len & 63) > _pathHopsVisible && msg->path_len != 0xFF) {
        const char* scrollHint = "W/S:Scrl";
        int scrollW = display.getTextWidth(scrollHint);
        display.setCursor((display.width() - scrollW) / 2, footerY);
        display.print(scrollHint);
      }
      const char* copyHint = "Ent:Copy";
#endif
      display.setCursor(display.width() - display.getTextWidth(copyHint) - 2, footerY);
      display.print(copyHint);

#ifdef USE_EINK
      return 5000;
#else
      return 1000;
#endif
    }
    
    if (channelMsgCount == 0) {
      display.setTextSize(0);  // Tiny font for body text
      display.setCursor(0, 20);
      display.setColor(DisplayDriver::LIGHT);
      display.print("No messages yet");
      display.setCursor(0, 30);
#if defined(LilyGo_T5S3_EPaper_Pro)
      display.print("Swipe: Switch channel");
      display.setCursor(0, 40);
      display.print("Long press: Compose");
#else
      display.print("A/D: Switch channel");
      display.setCursor(0, 40);
      display.print("C: Compose message");
#endif
      display.setTextSize(1);  // Restore for footer
    } else {
      display.setTextSize(0);  // Tiny font for message body
      int lineHeight = 9;   // 8px font + 1px spacing
      int headerHeight = 14;
      int footerHeight = 14;
      int scrollBarW = 4;   // Width of scroll indicator on right edge
      // Hard cutoff: no text may START at or beyond this y value
      // This ensures rendered glyphs (which extend lineHeight below y) stay above the footer
      int maxY = display.height() - footerHeight;
      
      int y = headerHeight;
      
      // Build list of messages for this channel (newest first)
      // Static to avoid 1200-byte stack allocation every render cycle
      static int channelMsgs[CHANNEL_MSG_HISTORY_SIZE];
      int numChannelMsgs = 0;
      
      for (int i = 0; i < _msgCount && numChannelMsgs < CHANNEL_MSG_HISTORY_SIZE; i++) {
        int idx = _newestIdx - i;
        while (idx < 0) idx += CHANNEL_MSG_HISTORY_SIZE;
        idx = idx % CHANNEL_MSG_HISTORY_SIZE;
        
        if (_messages[idx].valid && _messages[idx].channel_idx == _viewChannelIdx) {
          channelMsgs[numChannelMsgs++] = idx;
        }
      }
      
      // Reverse to chronological order (oldest first, newest last at bottom)
      for (int l = 0, r = numChannelMsgs - 1; l < r; l++, r--) {
        int tmp = channelMsgs[l]; channelMsgs[l] = channelMsgs[r]; channelMsgs[r] = tmp;
      }
      
      // Cache for reply select input bounds
      _replyChannelMsgCount = numChannelMsgs;
      
      // Clamp scroll position to valid range
      int maxScroll = numChannelMsgs > _msgsPerPage ? numChannelMsgs - _msgsPerPage : 0;
      if (_scrollPos > maxScroll) _scrollPos = maxScroll;
      
      // Calculate start index so newest messages appear at the bottom
      // scrollPos=0 shows the most recent messages, scrollPos++ scrolls up to older
      int startIdx = numChannelMsgs - _msgsPerPage - _scrollPos;
      if (startIdx < 0) startIdx = 0;
      
      // Display messages oldest-to-newest (top to bottom)
      int msgsDrawn = 0;
      bool screenFull = false;
      bool lastMsgTruncated = false;  // Did the last message get clipped by footer?
      for (int i = startIdx; i < numChannelMsgs && y + lineHeight <= maxY; i++) {
        int idx = channelMsgs[i];
        ChannelMessage* msg = &_messages[idx];
        
        // Reply select: is this the currently selected message?
        bool isSelected = (_replySelectMode && i == _replySelectPos);
        
        // Highlight: single fillRect for the entire message area, then
        // draw DARK text on top (same pattern as web reader bookmarks).
        // Because message height depends on word-wrap, we fill a generous
        // area up-front and erase the excess after rendering.
        int yStart = y;
        int contentW = display.width();
        int maxLinesPerMsg = 8;
        if (isSelected) {
          int maxFillH = (maxLinesPerMsg + 1) * lineHeight + 2;
          int availH = maxY - y;
          if (maxFillH > availH) maxFillH = availH;
          display.setColor(DisplayDriver::LIGHT);
          #if defined(LilyGo_T5S3_EPaper_Pro)
          display.fillRect(0, y, contentW, maxFillH);
#else
          display.fillRect(0, y + 5, contentW, maxFillH);
#endif
        }
        
        // Time indicator with hop count - inline on same line as message start
        display.setCursor(0, y);
        display.setColor(isSelected ? DisplayDriver::DARK : DisplayDriver::YELLOW);
        
        uint32_t age = _rtc->getCurrentTime() - msg->timestamp;
        if (isSelected) {
          // Show > marker for selected message, replacing the hop count
          if (age < 60) {
            sprintf(tmp, ">%ds ", age);
          } else if (age < 3600) {
            sprintf(tmp, ">%dm ", age / 60);
          } else if (age < 86400) {
            sprintf(tmp, ">%dh ", age / 3600);
          } else {
            sprintf(tmp, ">%dd ", age / 86400);
          }
        } else {
          if (age < 60) {
            sprintf(tmp, "(%d) %ds ", msg->path_len == 0xFF ? 0 : (msg->path_len & 63), age);
          } else if (age < 3600) {
            sprintf(tmp, "(%d) %dm ", msg->path_len == 0xFF ? 0 : (msg->path_len & 63), age / 60);
          } else if (age < 86400) {
            sprintf(tmp, "(%d) %dh ", msg->path_len == 0xFF ? 0 : (msg->path_len & 63), age / 3600);
          } else {
            sprintf(tmp, "(%d) %dd ", msg->path_len == 0xFF ? 0 : (msg->path_len & 63), age / 86400);
          }
        }
        display.print(tmp);
        // DO NOT advance y - message text continues on the same line
        
        // Message text with character wrapping and inline emoji support
        // (continues after timestamp on first line)
        display.setColor(isSelected ? DisplayDriver::DARK : DisplayDriver::LIGHT);
        
        int textLen = strlen(msg->text);
        int pos = 0;
        int linesForThisMsg = 0;
        char charStr[2] = {0, 0};
        
        // Track position in pixels for emoji placement
        // Uses advance width (cursor movement) not bounding box for px tracking
        int lineW = display.width() - scrollBarW - 1;  // Reserve space for scroll bar
        int px = display.getTextWidth(tmp);  // Pixel X after timestamp
        char dblStr[3] = {0, 0, 0};
        
        while (pos < textLen && linesForThisMsg < maxLinesPerMsg && y + lineHeight <= maxY) {
          uint8_t b = (uint8_t)msg->text[pos];
          
          if (b == EMOJI_PAD_BYTE) { pos++; continue; }
          
          // Word wrap: when starting a new text word, check if it fits
          if (b != ' ' && !isEmojiEscape(b) && px > 0) {
            bool boundary = (pos == 0);
            if (!boundary) {
              for (int bp = pos - 1; bp >= 0; bp--) {
                uint8_t pb = (uint8_t)msg->text[bp];
                if (pb == EMOJI_PAD_BYTE) continue;
                boundary = (pb == ' ' || isEmojiEscape(pb));
                break;
              }
            }
            if (boundary) {
              int wordW = 0;
              for (int j = pos; j < textLen; j++) {
                uint8_t wb = (uint8_t)msg->text[j];
                if (wb == EMOJI_PAD_BYTE) continue;
                if (wb == ' ' || isEmojiEscape(wb)) break;
                charStr[0] = (char)wb;
                dblStr[0] = dblStr[1] = (char)wb;
                int charAdv = display.getTextWidth(dblStr) - display.getTextWidth(charStr);
                if (charAdv < 1) charAdv = 1;
                wordW += charAdv;
              }
              if (px + wordW > lineW) {
                px = 0;
                linesForThisMsg++;
                y += lineHeight;
                if (linesForThisMsg >= maxLinesPerMsg || y + lineHeight > maxY) break;
              }
            }
          }
          
          if (isEmojiEscape(b)) {
            if (px + EMOJI_SM_W > lineW) {
              px = 0;
              linesForThisMsg++;
              y += lineHeight;
              if (linesForThisMsg >= maxLinesPerMsg || y + lineHeight > maxY) break;
            }
            const uint8_t* sprite = getEmojiSpriteSm(b);
            if (sprite) {
              display.drawXbm(px, y, sprite, EMOJI_SM_W, EMOJI_SM_H);
            }
            pos++;
            px += EMOJI_SM_W + 1;
            display.setCursor(px, y);
          } else if (b == ' ') {
            charStr[0] = ' ';
            dblStr[0] = dblStr[1] = ' ';
            int adv = display.getTextWidth(dblStr) - display.getTextWidth(charStr);
            if (adv < 1) adv = 1;  // Minimum advance (rounding fix for proportional fonts)
            if (px + adv > lineW) {
              px = 0;
              linesForThisMsg++;
              y += lineHeight;
              if (linesForThisMsg < maxLinesPerMsg && y + lineHeight <= maxY) {
                // skip trailing space at wrap
              } else break;
            } else {
              display.setCursor(px, y);
              display.print(charStr);
              px += adv;
            }
            pos++;
          } else {
            charStr[0] = (char)b;
            dblStr[0] = dblStr[1] = (char)b;
            int adv = display.getTextWidth(dblStr) - display.getTextWidth(charStr);
            if (adv < 1) adv = 1;  // Minimum advance (rounding fix for proportional fonts)
            if (px + adv > lineW) {
              px = 0;
              linesForThisMsg++;
              y += lineHeight;
              if (linesForThisMsg < maxLinesPerMsg && y + lineHeight <= maxY) {
                // continue to print below
              } else break;
            }
            display.setCursor(px, y);
            display.print(charStr);
            px += adv;
            pos++;
          }
        }
        
        // Check if this message was clipped (not all text rendered)
        lastMsgTruncated = (pos < textLen);
        
        // If we didn't end on a full line, still count it
        if (px > 0) {
          y += lineHeight;
        }
        
        y += 2;  // Small gap between messages
        
        // Erase excess highlight below the actual message.
        // The upfront fillRect covered a max area; restore the unused
        // portion back to background so subsequent messages render cleanly.
        if (isSelected) {
          int usedH = y - yStart;
          int maxFillH = (maxLinesPerMsg + 1) * lineHeight + 2;
          int availH = maxY - yStart;
          if (maxFillH > availH) maxFillH = availH;
          if (usedH < maxFillH) {
            display.setColor(DisplayDriver::DARK);
#if defined(LilyGo_T5S3_EPaper_Pro)
            display.fillRect(0, y, contentW, maxFillH - usedH);
#else
            display.fillRect(0, y + 5, contentW, maxFillH - usedH);
#endif
          }
        }
        
        msgsDrawn++;
        if (y + lineHeight > maxY) screenFull = true;
      }
      
      // Only update _msgsPerPage when at the bottom (scrollPos==0) and the
      // screen actually filled up.  While scrolled, freezing _msgsPerPage
      // prevents a feedback loop where variable-height messages cause
      // msgsPerPage to oscillate, shifting startIdx every render (flicker).
      if (screenFull && msgsDrawn > 0 && _scrollPos == 0) {
        // If the last message was truncated (text clipped by footer), exclude it
        // from the page count so next render starts one message later and the
        // bottom message fits completely.
        int effectiveDrawn = lastMsgTruncated ? msgsDrawn - 1 : msgsDrawn;
        if (effectiveDrawn < 1) effectiveDrawn = 1;
        _msgsPerPage = effectiveDrawn;
      }
      
      // --- Scroll bar (emoji picker style) ---
      int sbX = display.width() - scrollBarW;
      int sbTop = headerHeight;
      int sbHeight = maxY - headerHeight;
      
      // Draw track outline
      display.setColor(DisplayDriver::LIGHT);
      display.drawRect(sbX, sbTop, scrollBarW, sbHeight);
      
      if (channelMsgCount > _msgsPerPage) {
        // Scrollable: draw proportional thumb
        int maxScroll = channelMsgCount - _msgsPerPage;
        if (maxScroll < 1) maxScroll = 1;
        int thumbH = (_msgsPerPage * sbHeight) / channelMsgCount;
        if (thumbH < 4) thumbH = 4;
        // _scrollPos=0 is newest (bottom), so invert for thumb position
        int thumbY = sbTop + ((maxScroll - _scrollPos) * (sbHeight - thumbH)) / maxScroll;
        for (int ty = thumbY + 1; ty < thumbY + thumbH - 1; ty++)
          display.drawRect(sbX + 1, ty, scrollBarW - 2, 1);
      } else {
        // All messages fit: fill entire track
        for (int ty = sbTop + 1; ty < sbTop + sbHeight - 1; ty++)
          display.drawRect(sbX + 1, ty, scrollBarW - 2, 1);
      }
      
      display.setTextSize(1);  // Restore for footer
    }
    
    // Footer with controls
    int footerY = display.height() - 12;
    display.drawRect(0, footerY - 2, display.width(), 1);
    display.setCursor(0, footerY);
    display.setColor(DisplayDriver::YELLOW);
    
#if defined(LilyGo_T5S3_EPaper_Pro)
    display.setCursor(0, footerY);
    display.print("Swipe:Ch/Scroll");
    const char* midCh = "Tap:Path";
    display.setCursor((display.width() - display.getTextWidth(midCh)) / 2, footerY);
    display.print(midCh);
    const char* rtCh = "Hold:Compose";
    display.setCursor(display.width() - display.getTextWidth(rtCh) - 2, footerY);
    display.print(rtCh);
#else
    // Left side: abbreviated controls
    if (_replySelectMode) {
      display.print("W/S:Sel V:Pth Q:X");
      const char* rightText = "Ent:Reply";
      display.setCursor(display.width() - display.getTextWidth(rightText) - 2, footerY);
      display.print(rightText);
    } else {
      display.print("Q:Bck A/D:Ch R:Rply");
      const char* rightText = "Ent:New";
      display.setCursor(display.width() - display.getTextWidth(rightText) - 2, footerY);
      display.print(rightText);
    }
#endif

#ifdef USE_EINK
    return 5000;
#else
    return 1000;
#endif
  }

  bool handleInput(char c) override {
    // If overlay is showing, handle scroll and dismiss
    if (_showPathOverlay) {
      if (c == 'q' || c == 'Q' || c == '\b' || c == 'v' || c == 'V') {
        _showPathOverlay = false;
        _pathScrollPos = 0;
        return true;
      }
      if (c == '\r' || c == 13) {
        return false;  // Let main.cpp handle Enter for copy-to-compose
      }
      // W - scroll up in hop list
      if (c == 'w' || c == 'W' || c == 0xF2) {
        if (_pathScrollPos > 0) {
          _pathScrollPos--;
        }
        return true;
      }
      // S - scroll down in hop list
      if (c == 's' || c == 'S' || c == 0xF1) {
        ChannelMessage* msg = getNewestReceivedMsg();
        if (msg && msg->path_len > 0 && msg->path_len != 0xFF) {
          int totalHops = msg->path_len & 63;
          if (_pathScrollPos < totalHops - _pathHopsVisible) {
            _pathScrollPos++;
          }
        }
        return true;
      }
      return true;  // Consume all other keys while overlay is up
    }

    // --- Reply select mode ---
    if (_replySelectMode) {
      // Q - exit reply select
      if (c == 'q' || c == 'Q' || c == '\b') {
        _replySelectMode = false;
        _replySelectPos = -1;
        return true;
      }
      // W - select older message (lower index in chronological order)
      if (c == 'w' || c == 'W' || c == 0xF2) {
        if (_replySelectPos > 0) {
          _replySelectPos--;
          // Auto-scroll to keep selection visible
          int startIdx = _replyChannelMsgCount - _msgsPerPage - _scrollPos;
          if (startIdx < 0) startIdx = 0;
          if (_replySelectPos < startIdx) {
            _scrollPos++;
          }
        }
        return true;
      }
      // S - select newer message (higher index in chronological order)
      if (c == 's' || c == 'S' || c == 0xF1) {
        if (_replySelectPos < _replyChannelMsgCount - 1) {
          _replySelectPos++;
          // Auto-scroll to keep selection visible
          int endIdx = _replyChannelMsgCount - _scrollPos;
          if (_replySelectPos >= endIdx) {
            if (_scrollPos > 0) _scrollPos--;
          }
        }
        return true;
      }
      // V - view path for the SELECTED message (not just newest received)
      if (c == 'v' || c == 'V') {
        // Path overlay will use getNewestReceivedMsg() — for v1 this is fine.
        // The user can see the selected message's hop count in the > marker.
        ChannelMessage* selMsg = getReplySelectMsg();
        if (selMsg && selMsg->path_len != 0) {
          _showPathOverlay = true;
          _pathScrollPos = 0;
        }
        return true;
      }
      // Enter - let main.cpp handle (enters compose with @mention)
      if (c == '\r' || c == 13) {
        return false;
      }
      return true;  // Consume all other keys in reply select
    }

    int channelMsgCount = getMessageCountForChannel();

    // R - enter reply select mode
    if (c == 'r' || c == 'R') {
      if (channelMsgCount > 0) {
        _replySelectMode = true;
        // Start with newest message selected
        _replySelectPos = _replyChannelMsgCount > 0
                            ? _replyChannelMsgCount - 1 : channelMsgCount - 1;
        return true;
      }
      return false;
    }
    
    // V - show path detail for last received message
    if (c == 'v' || c == 'V') {
      if (getNewestReceivedMsg() != nullptr) {
        _showPathOverlay = true;
        _pathScrollPos = 0;
        return true;
      }
      return false;  // No received messages to show
    }
    
    // W or KEY_PREV - scroll up (older messages)
    if (c == 0xF2 || c == 'w' || c == 'W') {
      if (_scrollPos + _msgsPerPage < channelMsgCount) {
        _scrollPos++;
        return true;
      }
    }
    
    // S or KEY_NEXT - scroll down (newer messages)
    if (c == 0xF1 || c == 's' || c == 'S') {
      if (_scrollPos > 0) {
        _scrollPos--;
        return true;
      }
    }
    
    // A - previous channel
    if (c == 'a' || c == 'A') {
      _replySelectMode = false;
      _replySelectPos = -1;
      if (_viewChannelIdx > 0) {
        _viewChannelIdx--;
      } else {
        // Wrap to last valid channel
        for (uint8_t i = MAX_GROUP_CHANNELS - 1; i > 0; i--) {
          ChannelDetails ch;
          if (the_mesh.getChannel(i, ch) && ch.name[0] != '\0') {
            _viewChannelIdx = i;
            break;
          }
        }
      }
      _scrollPos = 0;
      markChannelRead(_viewChannelIdx);
      return true;
    }
    
    // D - next channel
    if (c == 'd' || c == 'D') {
      _replySelectMode = false;
      _replySelectPos = -1;
      ChannelDetails ch;
      uint8_t nextIdx = _viewChannelIdx + 1;
      if (the_mesh.getChannel(nextIdx, ch) && ch.name[0] != '\0') {
        _viewChannelIdx = nextIdx;
      } else {
        _viewChannelIdx = 0;
      }
      _scrollPos = 0;
      markChannelRead(_viewChannelIdx);
      return true;
    }
    
    return false;
  }
  
  // Reset scroll position to newest
  void resetScroll() {
    _scrollPos = 0;
  }
};