#include <chrono>
#include <cmath>
#include <csignal>
#include <cstdio>
#include <ctime>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <limits.h>
#include <sstream>
#include <string>
#include <sys/stat.h>
#include <thread>
#include <unistd.h>
#include <vector>

#include <arpa/inet.h>
#include <ifaddrs.h>
#include <netdb.h>

#include "graphics.h"
#include "led-matrix.h"

#include "weather.h"
#include "calendar.h"

using rgb_matrix::FrameCanvas;
using rgb_matrix::RGBMatrix;

static const uint8_t kHeart5x4[4] = {0b01010, 0b11111, 0b01110, 0b00100};

static bool have_weather_ok = false;
static std::chrono::steady_clock::time_point last_weather_ok;
static const auto WX_STALE_AFTER = std::chrono::minutes(20);

static bool have_calendar_ok = false;
static std::chrono::steady_clock::time_point last_calendar_ok;
static const auto CAL_STALE_AFTER = std::chrono::minutes(20);

static volatile sig_atomic_t g_running = 1;

static void InterruptHandler(int /*signum*/) { g_running = 0; }
static std::string DayLabel(bool tomorrow) {
  return tomorrow ? "TMRW" : "TODAY";
}

static std::string ReadConfigValue(const std::string &path,
                                  const std::string &key) {
  std::ifstream f(path);
  if (!f.is_open())
    return "";
  std::string line;
  while (std::getline(f, line)) {
    // strip comments
    auto hash = line.find('#');
    if (hash != std::string::npos)
      line = line.substr(0, hash);
    // trim
    auto ltrim = [](std::string &s) {
      while (!s.empty() && (s.front() == ' ' || s.front() == '\t' || s.front()=='\r'))
        s.erase(s.begin());
    };
    auto rtrim = [](std::string &s) {
      while (!s.empty() && (s.back() == ' ' || s.back() == '\t' || s.back()=='\r'))
        s.pop_back();
    };
    ltrim(line);
    rtrim(line);
    if (line.empty())
      continue;
    auto eq = line.find('=');
    if (eq == std::string::npos)
      continue;
    std::string k = line.substr(0, eq);
    std::string v = line.substr(eq + 1);
    ltrim(k); rtrim(k);
    ltrim(v); rtrim(v);
    if (k == key)
      return v;
  }
  return "";
}


static std::string GetHostname() {
  char buf[256];
  if (gethostname(buf, sizeof(buf)) == 0) {
    buf[sizeof(buf) - 1] = '\0';
    return std::string(buf);
  }
  return "unknown";
}

static std::string GetIPAddress() {
  struct ifaddrs *ifaddr;
  if (getifaddrs(&ifaddr) == -1)
    return "";

  std::string result;

  for (struct ifaddrs *ifa = ifaddr; ifa != NULL; ifa = ifa->ifa_next) {
    if (!ifa->ifa_addr)
      continue;

    if (ifa->ifa_addr->sa_family == AF_INET) {
      std::string name = ifa->ifa_name;

      // Skip loopback
      if (name == "lo")
        continue;

      char host[NI_MAXHOST];
      int s = getnameinfo(ifa->ifa_addr, sizeof(struct sockaddr_in), host,
                          NI_MAXHOST, NULL, 0, NI_NUMERICHOST);
      if (s == 0) {
        result = host;
        break; // take first non-loopback IPv4
      }
    }
  }

  freeifaddrs(ifaddr);
  return result;
}

static std::string GetUptimeString() {
  std::ifstream f("/proc/uptime");
  double seconds = 0.0;
  if (!(f >> seconds))
    return "";

  int s = (int)seconds;
  int days = s / 86400;
  s %= 86400;
  int hours = s / 3600;
  s %= 3600;
  int mins = s / 60;

  char buf[32];
  if (days > 0)
    snprintf(buf, sizeof(buf), "%dd %02dh", days, hours);
  else
    snprintf(buf, sizeof(buf), "%02dh %02dm", hours, mins);

  return std::string(buf);
}

static std::string NowTimeString() {
  std::time_t t = std::time(nullptr);
  std::tm tm{};
  localtime_r(&t, &tm);

  int hour24 = tm.tm_hour;
  int hour12 = hour24 % 12;
  if (hour12 == 0)
    hour12 = 12;

  int minute = tm.tm_min;
  bool pm = (hour24 >= 12);

  char buf[32];
  // Example: "9:22 PM"
  std::snprintf(buf, sizeof(buf), "%d:%02d %s", hour12, minute,
                pm ? "PM" : "AM");
  return std::string(buf);
}

static std::string NowDateString() {
  static const char *DOW[] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"};
  static const char *MON[] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun",
                              "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};

  std::time_t t = std::time(nullptr);
  std::tm tm{};
  localtime_r(&t, &tm);

  int wday = tm.tm_wday;
  int mon = tm.tm_mon;
  int mday = tm.tm_mday;

  char buf[32];
  // Example: "Thu Feb 12"
  std::snprintf(buf, sizeof(buf), "%s %s %d", DOW[wday], MON[mon], mday);
  return std::string(buf);
}
///////////////////////////////////////////////////////
// DrawBorder()
// Draw a border around each face
///////////////////////////////////////////////////////
static void DrawBorder(rgb_matrix::Canvas *c, int x0, int y0, int w, int h,
                       uint8_t r, uint8_t g, uint8_t b) {
  // Outer border
  for (int x = x0; x < x0 + w; x++) {
    c->SetPixel(x, y0, r, g, b);
    c->SetPixel(x, y0 + h - 1, r, g, b);
  }
  for (int y = y0; y < y0 + h; y++) {
    c->SetPixel(x0, y, r, g, b);
    c->SetPixel(x0 + w - 1, y, r, g, b);
  }

  // Inner border (subtle)
  uint8_t r2 = r / 3, g2 = g / 3, b2 = b / 3;
  for (int x = x0 + 1; x < x0 + w - 1; x++) {
    c->SetPixel(x, y0 + 1, r2, g2, b2);
    c->SetPixel(x, y0 + h - 2, r2, g2, b2);
  }
  for (int y = y0 + 1; y < y0 + h - 1; y++) {
    c->SetPixel(x0 + 1, y, r2, g2, b2);
    c->SetPixel(x0 + w - 2, y, r2, g2, b2);
  }
}
///////////////////////////////////////////////////////
// NowSecond()
// Timer
///////////////////////////////////////////////////////

static int NowSecond() {
  std::time_t t = std::time(nullptr);
  std::tm tm{};
  localtime_r(&t, &tm);
  return tm.tm_sec;
}
///////////////////////////////////////////////////////
// Heartbeat Support functions
// Pulse a heartbeat icon
///////////////////////////////////////////////////////
static uint8_t HeartbeatIntensity(std::chrono::steady_clock::time_point now,
                                  int period_ms = 1200) {
  auto ms = (int)std::chrono::duration_cast<std::chrono::milliseconds>(
                now.time_since_epoch())
                .count();
  int t = ms % period_ms;

  // 0..period, shape: quick rise, then longer decay
  // rise 0..120ms, decay 120..900ms, idle 900..1200ms
  if (t < 120) {
    return (uint8_t)(40 + (215 * t) / 120); // 40 -> 255
  }
  if (t < 900) {
    int td = t - 120;
    return (uint8_t)(255 - (215 * td) / 780); // 255 -> 40
  }
  return 0; // rest
}
static void DrawHeartbeatHeart(rgb_matrix::Canvas *canvas, int ox, int oy,
                               int face_w, rgb_matrix::Color base,
                               std::chrono::steady_clock::time_point now) {
  uint8_t a = HeartbeatIntensity(now);
  if (a == 0)
    return;

  // Scale base color by intensity
  uint8_t r = (uint8_t)((base.r * a) / 255);
  uint8_t g = (uint8_t)((base.g * a) / 255);
  uint8_t b = (uint8_t)((base.b * a) / 255);

  // Position, top right, inside border
  int x0 = ox + face_w - 6 - 5; // 6px inset, then 5px heart width
  int y0 = oy + 6;              // 6px from top

  for (int y = 0; y < 4; y++) {
    uint8_t row = kHeart5x4[y];
    for (int x = 0; x < 5; x++) {
      if (row & (1 << (4 - x))) {
        canvas->SetPixel(x0 + x, y0 + y, r, g, b);
      }
    }
  }
}

///////////////////////////////////////////////////////
// DrawProgressBar()
// Draws a progress bar on face
///////////////////////////////////////////////////////

static void DrawProgressBar(rgb_matrix::Canvas *c, int x0, int y0, int w, int h,
                            int value, int max_value) {
  if (max_value <= 0)
    return;
  if (value < 0)
    value = 0;
  if (value > max_value)
    value = max_value;

  // Border
  for (int x = x0; x < x0 + w; x++) {
    c->SetPixel(x, y0, 90, 90, 90);
    c->SetPixel(x, y0 + h - 1, 90, 90, 90);
  }
  for (int y = y0; y < y0 + h; y++) {
    c->SetPixel(x0, y, 90, 90, 90);
    c->SetPixel(x0 + w - 1, y, 90, 90, 90);
  }

  // Fill
  int inner_w = w - 2;
  int fill = (inner_w * value) / max_value;
  for (int x = 0; x < inner_w; x++) {
    for (int y = 1; y < h - 1; y++) {
      int r = 0;
      int g = 100 + (value * 2); // increases through minute
      int b = 255 - (value * 2);

      if (g > 255)
        g = 255;
      if (b < 0)
        b = 0;

      if (x < fill)
        c->SetPixel(x0 + 1 + x, y0 + y, r, g, b);
      else
        c->SetPixel(x0 + 1 + x, y0 + y, 20, 20, 20);
    }
  }
}

///////////////////////////////////////////////////////
// CenterTextX()
// Center a block of text along X axis
///////////////////////////////////////////////////////

static int CenterTextX(const rgb_matrix::Font &font, const std::string &text,
                       int face_width) {
  int width = 0;
  for (char ch : text) {
    width += font.CharacterWidth(ch);
  }
  int x = (face_width - width) / 2;
  if (x < 0)
    x = 0;
  return x;
}

///////////////////////////////////////////////////////
// TextWidth()
// calculate the lenght of a string of text
///////////////////////////////////////////////////////

static int TextWidth(const rgb_matrix::Font &font, const std::string &text) {
  int width = 0;
  for (char ch : text)
    width += font.CharacterWidth(ch);
  return width;
}

///////////////////////////////////////////////////////
// FitText()
// Fit Text  into screen wirth
///////////////////////////////////////////////////////

static std::string FitText(const rgb_matrix::Font &font,
                           const std::string &text, int max_width) {
  if (TextWidth(font, text) <= max_width)
    return text;

  std::string t = text;
  const std::string ell = "...";

  while (!t.empty() && TextWidth(font, t + ell) > max_width) {
    t.pop_back();
  }

  if (t.empty())
    return ell;
  return t + ell;
}

///////////////////////////////////////////////////////
// DrawUpArrow()
// as it says
///////////////////////////////////////////////////////

static void DrawUpArrow(rgb_matrix::Canvas *c, int x, int y, uint8_t r,
                        uint8_t g, uint8_t b) {
  // Small 5x5 up arrow, top-left at (x,y)
  c->SetPixel(x + 2, y + 0, r, g, b);
  c->SetPixel(x + 1, y + 1, r, g, b);
  c->SetPixel(x + 2, y + 1, r, g, b);
  c->SetPixel(x + 3, y + 1, r, g, b);
  c->SetPixel(x + 2, y + 2, r, g, b);
  c->SetPixel(x + 2, y + 3, r, g, b);
  c->SetPixel(x + 2, y + 4, r, g, b);
}
///////////////////////////////////////////////////////
// Draw Down Arrow
// As titled
///////////////////////////////////////////////////////

static void DrawDownArrow(rgb_matrix::Canvas *c, int x, int y, uint8_t r,
                          uint8_t g, uint8_t b) {
  // Small 5x5 down arrow, top-left at (x,y)
  c->SetPixel(x + 2, y + 4, r, g, b);
  c->SetPixel(x + 1, y + 3, r, g, b);
  c->SetPixel(x + 2, y + 3, r, g, b);
  c->SetPixel(x + 3, y + 3, r, g, b);
  c->SetPixel(x + 2, y + 2, r, g, b);
  c->SetPixel(x + 2, y + 1, r, g, b);
  c->SetPixel(x + 2, y + 0, r, g, b);
}

///////////////////////////////////////////////////////
// set a pallette based on colors from Open_Meteo
//
///////////////////////////////////////////////////////

static rgb_matrix::Color WeatherThemeColor(int code) {
  // Simple palette based on Open-Meteo weather codes
  if (code == 0)
    return rgb_matrix::Color(255, 200, 0); // clear = warm sun
  if (code == 1 || code == 2)
    return rgb_matrix::Color(120, 200, 255); // mostly/partly = sky
  if (code == 3)
    return rgb_matrix::Color(160, 160, 180); // overcast
  if (code == 45 || code == 48)
    return rgb_matrix::Color(140, 140, 160); // fog
  if ((code >= 51 && code <= 67) || (code >= 80 && code <= 82))
    return rgb_matrix::Color(0, 180, 255); // rain/showers
  if (code >= 71 && code <= 77)
    return rgb_matrix::Color(200, 240, 255); // snow
  if (code >= 95)
    return rgb_matrix::Color(255, 80, 120); // storms
  return rgb_matrix::Color(180, 180, 180);
}

///////////////////////////////////////////////////////
// DrawCircle()
//  draw a circle on face
///////////////////////////////////////////////////////

static void DrawCircle(rgb_matrix::Canvas *c, int cx, int cy, int r, uint8_t R,
                       uint8_t G, uint8_t B) {
  for (int y = -r; y <= r; y++) {
    for (int x = -r; x <= r; x++) {
      if (x * x + y * y <= r * r)
        c->SetPixel(cx + x, cy + y, R, G, B);
    }
  }
}

static inline void SetPixelClipped(rgb_matrix::Canvas *c, int x, int y,
                                   uint8_t r, uint8_t g, uint8_t b, int x0,
                                   int y0, int x1, int y1) {
  if (x < x0 || x > x1 || y < y0 || y > y1)
    return;
  c->SetPixel(x, y, r, g, b);
}

static void DrawCircleClipped(rgb_matrix::Canvas *c, int cx, int cy, int r,
                              uint8_t rr, uint8_t gg, uint8_t bb, int x0,
                              int y0, int x1, int y1) {
  // naive filled circle, clipped
  for (int y = -r; y <= r; y++) {
    for (int x = -r; x <= r; x++) {
      if (x * x + y * y <= r * r) {
        SetPixelClipped(c, cx + x, cy + y, rr, gg, bb, x0, y0, x1, y1);
      }
    }
  }
}

///////////////////////////////////////////////////////
// DrawWeatherIcon()
// Draw the weather icon based on Code from Open-Meteo
///////////////////////////////////////////////////////
static void DrawWeatherIcon(rgb_matrix::Canvas *c, int ox, int oy, int code) {
  const int x0 = ox;
  const int y0 = oy;
  const int x1 = ox + 15;
  const int y1 = oy + 15;

  auto SP = [&](int x, int y, uint8_t r, uint8_t g, uint8_t b) {
    SetPixelClipped(c, x, y, r, g, b, x0, y0, x1, y1);
  };

  auto CC = [&](int cx, int cy, int rad, uint8_t r, uint8_t g, uint8_t b) {
    DrawCircleClipped(c, cx, cy, rad, r, g, b, x0, y0, x1, y1);
  };

  if (code == 0 || code == 1 || code == 2) {
    CC(ox + 8, oy + 8, 5, 255, 200, 0);
    for (int i = 0; i < 16; i += 4) {
      SP(ox + 8, oy + i, 255, 200, 0);
      SP(ox + i, oy + 8, 255, 200, 0);
    }
    return;
  }

  bool fog = (code == 45 || code == 48);
  bool snowy = ((code >= 71 && code <= 77) || (code >= 85 && code <= 86));

  uint8_t cr = 180, cg = 180, cb = 200;
  CC(ox + 6, oy + 9, 4, cr, cg, cb);
  CC(ox + 10, oy + 8, 5, cr, cg, cb);

  for (int x = 2; x <= 14; x++) {
    for (int y = 10; y <= 13; y++) {
      SP(ox + x, oy + y, cr, cg, cb);
    }
  }

  if (fog) {
    for (int yy = 14; yy <= 15; yy++) {
      for (int xx = 3; xx <= 13; xx += 2) {
        SP(ox + xx, oy + yy, 140, 140, 160);
      }
    }
  }

  if (snowy) {
    for (int i = 0; i < 3; i++) {
      int x = ox + 5 + i * 4;
      SP(x, oy + 15, 220, 220, 255);
      SP(x + 1, oy + 14, 220, 220, 255);
    }
  }

  bool rainy = ((code >= 51 && code <= 67) || (code >= 80 && code <= 82));
  bool storm = (code >= 95);

  if (rainy) {
    for (int i = 0; i < 3; i++) {
      int x = ox + 5 + i * 4;
      SP(x, oy + 15, 0, 180, 255);
      SP(x + 1, oy + 14, 0, 180, 255);
    }
  }

  if (storm) {
    SP(ox + 9, oy + 14, 255, 220, 0);
    SP(ox + 8, oy + 15, 255, 220, 0);
    SP(ox + 10, oy + 13, 255, 220, 0);
  }
}

///////////////////////////////////////////////////////
// DrawFaceFrameAndTitle()
// Draw border and title on face
///////////////////////////////////////////////////////

static void DrawFaceFrameAndTitle(rgb_matrix::Canvas *canvas,
                                  const rgb_matrix::Font &font_small, int ox,
                                  int oy, int face_w, int face_h,
                                  const std::string &title,
                                  const WeatherData &weather) {
  DrawBorder(canvas, ox, oy, face_w, face_h, 120, 120, 120);

  rgb_matrix::Color title_color(200, 200, 200);
  if (title == "CLOCK")
    title_color = rgb_matrix::Color(0, 180, 255);
  if (title == "WEATHER" && weather.ok)
    title_color = WeatherThemeColor(weather.code);
  if (title == "FORECAST" && weather.ok)
    title_color = WeatherThemeColor(weather.today_code ? weather.today_code
                                                       : weather.code);

  int tx = ox + CenterTextX(font_small, title, face_w);
  rgb_matrix::DrawText(canvas, font_small, tx, oy + 12, title_color, nullptr,
                       title.c_str(), 0);
}

///////////////////////////////////////////////////////
// DrawDebugLabel()
// Draw Face Name on face
///////////////////////////////////////////////////////
static void DrawFaceDebugLabel(rgb_matrix::Canvas *canvas,
                               const rgb_matrix::Font &font_small, int ox,
                               int oy, int face_index) {
  std::ostringstream ss;
  ss << "FACE " << (face_index + 1);
  std::string s = ss.str();
  rgb_matrix::DrawText(canvas, font_small, ox + 4, oy + 62,
                       rgb_matrix::Color(120, 140, 155), nullptr, s.c_str(), 0);
}

///////////////////////////////////////////////////////
// DrawWeatherFace()
// Weather Face
///////////////////////////////////////////////////////
static void DrawFaceWeather(rgb_matrix::Canvas *canvas,
                            const rgb_matrix::Font &font_big,
                            const rgb_matrix::Font &font_small,
                            const rgb_matrix::Font &font_tiny, int ox, int oy,
                            int face_w, const WeatherData &weather) {
  rgb_matrix::Color theme = WeatherThemeColor(weather.code);

  //////////////////////////////////////////////////////////
  // ox - 64* face number, oy =  Pixel row (0)
  /////////////////////////////////////////////////////////

  // Icon
  int icon_x = ox + 4;
  int icon_y = oy + 20;  // keep icon fully within face bounds
  DrawWeatherIcon(canvas, icon_x, icon_y, weather.code);

  // Temp (right of icon lane)
  std::ostringstream ts;
  ts << (int)std::round(weather.temp) << "F";
  std::string temp_txt = ts.str();

  int content_x0 = ox + 22;
  int content_w = face_w - 24;

  // Temperature
  int x_temp = ox + CenterTextX(font_big, temp_txt, face_w);
  rgb_matrix::DrawText(canvas, font_big, x_temp, oy + 38,
                       rgb_matrix::Color(255, 255, 255), nullptr,
                       temp_txt.c_str(), 0);

  // Condition label
  std::string cond = WeatherLabelFromCode(weather.code);
  std::string lbl = FitText(font_tiny, cond, content_w);
  //    std::cout << "Condition: " << lbl << std::endl;
  x_temp = ox + CenterTextX(font_tiny, lbl, face_w);
  rgb_matrix::DrawText(canvas, font_tiny, x_temp, oy + 20,
                       rgb_matrix::Color(255, 255, 255), nullptr, lbl.c_str(),
                       0);

  // Hi / Lo centered lanes with arrows
  int baseline = oy + 60;
  std::string hi_num = std::to_string((int)std::round(weather.hi));
  std::string lo_num = std::to_string((int)std::round(weather.lo));

  int hi_w = TextWidth(font_small, hi_num);
  int lo_w = TextWidth(font_small, lo_num);

  int mid = ox + face_w / 2;
  int gap = 4;

  int hi_x = (mid - gap) - hi_w;
  int lo_x = (mid + gap);

  rgb_matrix::DrawText(canvas, font_small, hi_x, baseline,
                       rgb_matrix::Color(140, 140, 140), nullptr,
                       hi_num.c_str(), 0);

  rgb_matrix::DrawText(canvas, font_small, lo_x, baseline,
                       rgb_matrix::Color(140, 140, 140), nullptr,
                       lo_num.c_str(), 0);

  int arrow_y = baseline - 7;
  DrawUpArrow(canvas, hi_x + hi_w + 2, arrow_y, 255, 200, 0);
  DrawDownArrow(canvas, lo_x + lo_w + 2, arrow_y, 0, 180, 255);
}

/////////////////////////////////////////////////////////////////
// Draw ForecastRow()
// Draw a row on the forecast face
/////////////////////////////////////////////////////////////////

static void DrawForecastRow(rgb_matrix::Canvas *canvas,
                            const rgb_matrix::Font &font_small,
                            const rgb_matrix::Font &font_tiny, int ox, int oy,
                            int face_w, int y1, int y2, const char *day_label,
                            bool tomorrow, int code, int pop, int hi, int lo) {

  // Icon
  int icon_x = ox + 4;
  int icon_y = y1 - 11; // was -12
  DrawWeatherIcon(canvas, icon_x, icon_y, code);

  // Theme color
  rgb_matrix::Color theme = WeatherThemeColor(code);
  rgb_matrix::Color dim_theme = theme;
  if (tomorrow) {
    dim_theme = rgb_matrix::Color(theme.r * 0.7, theme.g * 0.7, theme.b * 0.7);
  }

  // Text layout helpers (define once, up front)
  const int text_x0 = ox + 24;
  const int right_pad = 4;
  const int right_x = ox + face_w - right_pad; // safe rightmost pixel for text

  // ---- LINE 1: day label + condition + PoP (safe right clamp) ----
  std::string pop_s = std::to_string(pop) + "%";
  int pop_w = TextWidth(font_tiny, pop_s);

  // Anchor PoP so its right edge never extends past right_x
  int pop_x = right_x - pop_w;
  if (pop_x < text_x0)
    pop_x = text_x0;

  // Condition label
  std::string cond = WeatherLabelFromCode(code);
  // Fit condition into the space between the day label and the PoP column.
  int label_w = TextWidth(font_tiny, day_label);
  int cond_x = text_x0 + label_w + 3; // 3px gap after label
  int cond_max_w = pop_x - 3 - cond_x; // reserve 3px gap before PoP
  if (cond_max_w < 0)
    cond_max_w = 0;
  std::string lbl = FitText(font_tiny, cond, cond_max_w);

  // Draw line 1
  rgb_matrix::DrawText(canvas, font_tiny, text_x0 - 2, y1,
                       rgb_matrix::Color(170, 170, 170), nullptr, day_label, 0);

  rgb_matrix::DrawText(canvas, font_tiny, cond_x, y1 + 7, dim_theme,
                       nullptr, lbl.c_str(), 0);

  rgb_matrix::Color pop_col(120, 200, 255);
  rgb_matrix::DrawText(canvas, font_tiny, pop_x, y1, pop_col, nullptr,
                       pop_s.c_str(), 0);

  // ---- LINE 2: temps (centered within safe area, clamped) ----
  rgb_matrix::Color high_col(255, 140, 0);
  rgb_matrix::Color low_col(100, 160, 255);
  if (tomorrow) {
    high_col = rgb_matrix::Color(180, 90, 0);
    low_col = rgb_matrix::Color(70, 120, 200);
  }

  // Safe horizontal bounds for temps (text_x0 .. right_x)
  int safe_left = text_x0;
  int safe_right = right_x;

  // Center within the safe area
  int center_x = (safe_left + safe_right) / 2;

  std::string hi_s = std::to_string(hi);
  std::string lo_s = std::to_string(lo);

  int hi_w = TextWidth(font_small, hi_s);
  int lo_w = TextWidth(font_small, lo_s);
  int gap = 4;

  int hi_x = center_x - gap - hi_w;
  int lo_x = center_x + gap;

  // Clamp to safe area
  if (hi_x < safe_left)
    hi_x = safe_left;
  if (lo_x + lo_w > safe_right)
    lo_x = safe_right - lo_w;
  if (lo_x < safe_left)
    lo_x = safe_left;

  rgb_matrix::DrawText(canvas, font_tiny, hi_x, y2 + 1, high_col, nullptr,
                       hi_s.c_str(), 0);

  rgb_matrix::DrawText(canvas, font_tiny, lo_x, y2 + 1, low_col, nullptr,
                       lo_s.c_str(), 0);
}

///////////////////////////////////////////////////////
// DrawForecastFace()
// Draw the weather Forecast
///////////////////////////////////////////////////////

static void DrawFaceForecast(rgb_matrix::Canvas *canvas,
                             const rgb_matrix::Font &font_small,
                             const rgb_matrix::Font &font_tiny, int ox, int oy,
                             int face_w, const WeatherData &weather) {

  if (!weather.ok) {
    std::string msg = "No data";
    int x = ox + CenterTextX(font_small, msg, face_w);
    rgb_matrix::DrawText(canvas, font_small, x, oy + 40,
                         rgb_matrix::Color(180, 60, 60), nullptr, msg.c_str(),
                         0);
    return;
  }

  // Two rows
  // Row tops are where icon draws (top-left of icon)
  int y1_today = oy + 22;
  int y2_today = oy + 32;

  int divider_y = oy + 34;

  int y1_tmrw = oy + 46;
  int y2_tmrw = oy + 56;

  // divider
  for (int x = ox + 4; x < ox + face_w - 4; x++) {
    canvas->SetPixel(x, divider_y, 40, 40, 40);
  }

  DrawForecastRow(canvas, font_small, font_tiny, ox, oy, face_w, y1_today,
                  y2_today, "TODAY", false,
                  weather.today_code ? weather.today_code : weather.code,
                  weather.today_pop, (int)std::round(weather.hi),
                  (int)std::round(weather.lo));

  DrawForecastRow(canvas, font_small, font_tiny, ox, oy, face_w, y1_tmrw,
                  y2_tmrw, "TMRW", true,
                  weather.tomorrow_code ? weather.tomorrow_code : weather.code,
                  weather.tomorrow_pop, (int)std::round(weather.tomorrow_hi),
                  (int)std::round(weather.tomorrow_lo));
}

///////////////////////////////////////////////////////
// NET Graph
///////////////////////////////////////////////////////
static int ReadNetKBps() {
  static unsigned long long prev_total = 0;
  static bool have_prev = false;

  std::ifstream f("/proc/net/dev");
  if (!f.is_open())
    return 0;

  std::string line;
  unsigned long long total = 0;

  while (std::getline(f, line)) {
    std::istringstream iss(line);
    std::string iface;

    if (!(iss >> iface))
      continue;

    // Skip headers and loopback
    if (iface.find("lo:") != std::string::npos)
      continue;
    if (iface.find(":") == std::string::npos)
      continue;

    // Remove trailing colon
    iface = iface.substr(0, iface.size() - 1);

    unsigned long long rx_bytes = 0;
    unsigned long long tx_bytes = 0;

    iss >> rx_bytes;
    // skip 7 RX fields
    for (int i = 0; i < 7; i++)
      iss >> line;

    iss >> tx_bytes;

    total += rx_bytes + tx_bytes;
  }

  if (!have_prev) {
    prev_total = total;
    have_prev = true;
    return 0;
  }

  unsigned long long delta = total - prev_total;
  prev_total = total;

  // bytes/sec → KB/sec
  return (int)(delta / 1024);
}

///////////////////////////////////////////////////////
// Data and structure for CPU grapb
///////////////////////////////////////////////////////
struct StatusData {
  int cpu_pct = 0;
  int mem_pct = 0;
  int temp_c = 0;
  int net_kbps = 0;

  std::string hostname;
  std::string ip;
  std::string uptime_str;
  std::string version; // "v1.0.0"

  int brightness_pct = 40; // or whatever you set
  int rotate_sec = 15;

  bool weather_ok = false;
  bool weather_stale = false;

  bool calendar_ok = false;
  bool calendar_stale = false;

  bool net_ok = true;
};

static StatusData status;
static CalendarEvent cal_event;

// Simple 56-sample ring buffer
static const int kGraphWidth = 56;
static int cpu_samples[kGraphWidth] = {0};
static int cpu_index = 0;
static int cpu_count = 0;

static int temp_samples[kGraphWidth] = {0};
static int temp_index = 0;
static int temp_count = 0;

static int mem_samples[kGraphWidth] = {0};
static int mem_index = 0;
static int mem_count = 0;

static int net_samples[kGraphWidth] = {0};
static int net_index = 0;
static int net_count = 0;

static inline void PushSample(int *samples, int &index, int &count, int v) {
  samples[index] = v;
  index = (index + 1) % kGraphWidth;
  if (count < kGraphWidth)
    count++;
}

///////////////////////////////////////////////////////
// DrawMiniGraph()
// Draw the graphs on the Status Face
///////////////////////////////////////////////////////
static void DrawMiniGraph(rgb_matrix::Canvas *canvas, int x0, int base_y, int w,
                          int h, const int *samples, int index, int count,
                          int vmax, bool use_threshold_colors) {
  int draw_w = std::min(w, count);
  for (int i = 0; i < draw_w; i++) {
    int idx = (index - draw_w + i + kGraphWidth) % kGraphWidth;
    int val = samples[idx];

    // scale to height, val in [0..vmax]
    if (val < 0)
      val = 0;
    if (val > vmax)
      val = vmax;

    int bar_h = (val * h + (vmax - 1)) / vmax; // round up

    rgb_matrix::Color c(120, 200, 255); // default (for TMP maybe)
    if (use_threshold_colors) {
      c = (val < 60)   ? rgb_matrix::Color(0, 255, 0)
          : (val < 85) ? rgb_matrix::Color(255, 255, 0)
                       : rgb_matrix::Color(255, 0, 0);
    }

    for (int y = 0; y < bar_h; y++) {
      canvas->SetPixel(x0 + i, base_y - y, c.r, c.g, c.b);
    }
  }
}

static rgb_matrix::Color HealthColor(bool ok, bool stale) {
  if (!ok)
    return rgb_matrix::Color(255, 80, 80);
  if (stale)
    return rgb_matrix::Color(255, 220, 80);
  return rgb_matrix::Color(80, 220, 120);
}

///////////////////////////////////////////////////////
// DrawSystemFace()
// System
///////////////////////////////////////////////////////
static void DrawFaceSystem(rgb_matrix::FrameCanvas *canvas,
                           const rgb_matrix::Font &font_small,
                           const rgb_matrix::Font &font_tiny, int ox, int oy,
                           int face_w, int face_h, const StatusData &status,
                           int rotate_sec, int brightness_pct,
                           const std::string &version,
                           std::chrono::steady_clock::time_point now) {
  // Row baselines, centered rhythm
  const int y_title2 = oy + 20;
  const int y_cfg1 = oy + 28;
  const int y_cfg2 = oy + 36;
  const int y_d1 = oy + 44;
  const int y_d2 = oy + 52;
  const int y_up = oy + 60;

  // Line 1: product and version

  std::string line1 = std::string("InfoCube v") + INFOCUBE_VERSION + " " + INFOCUBE_GIT_COMMIT;
  line1 = FitText(font_tiny, line1, face_w - 6);

  std::string line2 = std::string("Built ") + INFOCUBE_BUILD_DATE;
  line2 = FitText(font_tiny, line2, face_w - 6);

  int x1 = ox + CenterTextX(font_tiny, line1, face_w);
  rgb_matrix::DrawText(canvas, font_tiny, x1, y_title2,
                       rgb_matrix::Color(200, 200, 200), nullptr, line1.c_str(),
                       0);

  // Line 2 and 3: config
  char buf[64];

  snprintf(buf, sizeof(buf), "Rotate %ds", rotate_sec);
  std::string cfg1 = buf;
  int x2 = ox + CenterTextX(font_tiny, cfg1, face_w);
  rgb_matrix::DrawText(canvas, font_tiny, x2, y_cfg1,
                       rgb_matrix::Color(160, 160, 160), nullptr, cfg1.c_str(),
                       0);

  snprintf(buf, sizeof(buf), "Bright %d%%", brightness_pct);
  std::string cfg2 = buf;
  int x3 = ox + CenterTextX(font_tiny, cfg2, face_w);
  rgb_matrix::DrawText(canvas, font_tiny, x3, y_cfg2,
                       rgb_matrix::Color(160, 160, 160), nullptr, cfg2.c_str(),
                       0);

  // Diagnostics
  auto diag_line = [&](const std::string &label, bool ok, bool stale) {
    std::string s = label + " " + (ok ? (stale ? "STALE" : "OK") : "DOWN");
    s = FitText(font_tiny, s, face_w - 8);
    return s;
  };

  std::string d1 = diag_line("Wx", status.weather_ok, status.weather_stale);
  std::string d2 = diag_line("Cal", status.calendar_ok, status.calendar_stale);
  std::string d3;
  if (!status.net_ok)
    d3 = "Net DOWN";
  else
    d3 = "Net OK";
  d3 = FitText(font_tiny, d3, face_w - 8);

  int xd1 = ox + CenterTextX(font_tiny, d1, face_w);
  int xd2 = ox + CenterTextX(font_tiny, d2, face_w);
  int xd3 = ox + CenterTextX(font_tiny, d3, face_w);

  rgb_matrix::DrawText(canvas, font_tiny, xd1, y_d1,
                       HealthColor(status.weather_ok, status.weather_stale),
                       nullptr, d1.c_str(), 0);

  rgb_matrix::DrawText(canvas, font_tiny, xd2, y_d2,
                       HealthColor(status.calendar_ok, status.calendar_stale),
                       nullptr, d2.c_str(), 0);
  // Third diagnostic line shares the bottom row with uptime, so keep it one row
  // above uptime
  rgb_matrix::DrawText(canvas, font_tiny, xd3, oy + 60,
                       status.net_ok ? rgb_matrix::Color(80, 220, 120)
                                     : rgb_matrix::Color(255, 80, 80),
                       nullptr, d3.c_str(), 0);

  // Bottom: uptime
  //  std::string up = "Up " + status.uptime_str;
  //  up = FitText(font_tiny, up, face_w - 8);
  //  int xup = ox + CenterTextX(font_tiny, up, face_w);
  //  rgb_matrix::DrawText(canvas, font_tiny, xup, y_up,
  //                       rgb_matrix::Color(160,160,160),
  //                       nullptr, up.c_str(), 0);

  // Heartbeat dot, top right, color reflects overall health
  bool overall_ok = status.net_ok && status.weather_ok && status.calendar_ok;
  bool overall_stale = status.weather_stale || status.calendar_stale;
  rgb_matrix::Color hb = HealthColor(overall_ok, overall_stale);
  DrawHeartbeatHeart(canvas, ox, oy, face_w, hb, now);
}

///////////////////////////////////////////////////////
// DrawMessageFace()
// MESSAGE - next upcoming calendar event
///////////////////////////////////////////////////////
static void SplitTitleTwoLines(const rgb_matrix::Font &font_small,
                               const rgb_matrix::Font &font_tiny,
                               int face_w, const std::string &title,
                               std::string *l1, std::string *l2) {
  // Prefer a clean word split that fits in two lines.
  std::string t = title;
  if (t.empty()) t = "No upcoming events";

  // If it already fits on one line, keep it.
  std::string one = FitText(font_small, t, face_w - 8);
  if (one == t) {
    *l1 = one;
    *l2 = "";
    return;
  }

  // Word-split
  std::istringstream iss(t);
  std::vector<std::string> words;
  for (std::string w; iss >> w;) words.push_back(w);

  std::string best1, best2;
  for (size_t split = 1; split < words.size(); ++split) {
    std::string a, b;
    for (size_t i = 0; i < split; ++i) {
      if (!a.empty()) a += " ";
      a += words[i];
    }
    for (size_t i = split; i < words.size(); ++i) {
      if (!b.empty()) b += " ";
      b += words[i];
    }
    a = FitText(font_small, a, face_w - 8);
    b = FitText(font_small, b, face_w - 8);

    // Choose first split where both are "reasonably full" and second isn't empty.
    if (!b.empty()) {
      best1 = a;
      best2 = b;
      break;
    }
  }

  if (best1.empty()) {
    // Fallback: hard-fit first line, remainder second line
    best1 = FitText(font_small, t, face_w - 8);
    best2 = "";
  }
  *l1 = best1;
  *l2 = best2;
}

void DrawFaceMessage(rgb_matrix::FrameCanvas *canvas,
                     const rgb_matrix::Font &font_small,
                     const rgb_matrix::Font &font_tiny,
                     int ox, int oy, int face_w, int face_h,
                     const CalendarEvent &ev,
                     const StatusData &status) {
  // Vertical rhythm: title band is handled elsewhere (top ~16px).
  const int y1 = oy + 28;  // title line 1
  const int y2 = oy + 40;  // title line 2 (optional)
  const int y_when = oy + 52;
  const int y_footer = oy + 60;

  std::string l1, l2;
  SplitTitleTwoLines(font_small, font_tiny, face_w, ev.title, &l1, &l2);

  int x1 = ox + CenterTextX(font_small, l1, face_w);
  rgb_matrix::DrawText(canvas, font_small, x1, y1,
                       rgb_matrix::Color(255, 255, 255), nullptr,
                       l1.c_str(), 0);

  if (!l2.empty()) {
    int x2 = ox + CenterTextX(font_small, l2, face_w);
    rgb_matrix::DrawText(canvas, font_small, x2, y2,
                         rgb_matrix::Color(220, 220, 220), nullptr,
                         l2.c_str(), 0);
  }

  std::string when = ev.when.empty() ? "No events" : ev.when;
  when = FitText(font_tiny, when, face_w - 8);
  int xw = ox + CenterTextX(font_tiny, when, face_w);
  rgb_matrix::DrawText(canvas, font_tiny, xw, y_when,
                       rgb_matrix::Color(160, 160, 160), nullptr,
                       when.c_str(), 0);

  // Footer: Cal status left, age right
  std::string cal_s = std::string("Cal ") + (status.calendar_ok ? (status.calendar_stale ? "STALE" : "OK") : "DOWN");
  cal_s = FitText(font_tiny, cal_s, face_w - 8);
  rgb_matrix::DrawText(canvas, font_tiny, ox + 4, y_footer,
                       HealthColor(status.calendar_ok, status.calendar_stale),
                       nullptr, cal_s.c_str(), 0);

  // Age (right)
  std::string age;
  if (ev.fetched_at != 0) {
    std::time_t nowt = std::time(nullptr);
    int mins = (int)std::difftime(nowt, ev.fetched_at) / 60;
    if (mins < 0) mins = 0;
    if (mins < 60) {
      age = std::to_string(mins) + "m";
    } else {
      int hrs = mins / 60;
      age = std::to_string(hrs) + "h";
    }
  } else {
    age = "";
  }
  age = FitText(font_tiny, age, face_w - 8);
  int xa = ox + face_w - 4 - TextWidth(font_tiny, age);
  rgb_matrix::DrawText(canvas, font_tiny, xa, y_footer,
                       rgb_matrix::Color(120, 120, 120), nullptr,
                       age.c_str(), 0);
}

///////////////////////////////////////////////////////
// DrawStatusFace()
// Status
///////////////////////////////////////////////////////
void DrawFaceStatus(rgb_matrix::FrameCanvas *canvas,
                    rgb_matrix::Font &font_small, rgb_matrix::Font &font_tiny,
                    int ox, int oy, int face_w, int face_h) {
  const int left_x = ox + 4;

  // Right-column graph region
  const int graph_x0 = ox + 34;
  const int graph_x1 = ox + face_w - 8; // keep inside border
  const int graph_w = graph_x1 - graph_x0 + 1;

  const int graph_height = 6;
  const int graph_base_y = oy + 26;
  const int graph_h = graph_height;

  // Text baselines (5x8)
  const int y_cpu = oy + 27;
  const int y_tmp = oy + 35;
  const int y_mem = oy + 43;
  const int y_net = oy + 51;
  // reserve these for later:
  // const int y_net = oy + 51;
  // const int y_up  = oy + 59;

  // Hostname + IP (single line under STATUS title)
  std::string host_name = status.hostname;
  std::string host_ip = status.ip;
  // print host name
  host_name = FitText(font_tiny, host_name, face_w - 8);
  int pos_x = ox + CenterTextX(font_tiny, host_name, face_w);
  rgb_matrix::DrawText(canvas, font_tiny, pos_x, oy + 20,
                       rgb_matrix::Color(160, 160, 160), nullptr,
                       host_name.c_str(), 0);
  // print host ip
  host_ip = FitText(font_tiny, host_ip, face_w);
  pos_x = ox + CenterTextX(font_tiny, host_ip, face_w);
  rgb_matrix::DrawText(canvas, font_tiny, pos_x, oy + 63,
                       rgb_matrix::Color(160, 160, 160), nullptr,
                       host_ip.c_str(), 0);

  char buf[32];

  snprintf(buf, sizeof(buf), "CPU %d%%", status.cpu_pct);
  rgb_matrix::DrawText(canvas, font_tiny, left_x, y_cpu,
                       rgb_matrix::Color(200, 200, 200), nullptr, buf, 0);

  snprintf(buf, sizeof(buf), "TMP %dC", status.temp_c);
  rgb_matrix::DrawText(canvas, font_tiny, left_x, y_tmp,
                       rgb_matrix::Color(200, 200, 200), nullptr, buf, 0);

  snprintf(buf, sizeof(buf), "MEM %d%%", status.mem_pct);
  rgb_matrix::DrawText(canvas, font_tiny, left_x, y_mem,
                       rgb_matrix::Color(200, 200, 200), nullptr, buf, 0);

  snprintf(buf, sizeof(buf), "NET %dK", status.net_kbps);
  rgb_matrix::DrawText(canvas, font_tiny, left_x, y_net,
                       rgb_matrix::Color(200, 200, 200), nullptr, buf, 0);

  // Graph: draw only within graph_w bars
  // Mini-graphs to the right of each stat line
  const int gx = graph_x0 + 5;
  const int gw = graph_w;
  const int gh = graph_h;

  // CPU graph (0..100, threshold colors)
  DrawMiniGraph(canvas, gx, graph_base_y, gw, gh, cpu_samples, cpu_index,
                cpu_count, 100, true);

  // TMP graph (scale to a reasonable max; adjust 80 if you want)
  DrawMiniGraph(canvas, gx, y_tmp - 1, gw, gh, temp_samples, temp_index,
                temp_count, 80, false);

  // MEM graph (0..100, threshold colors)
  DrawMiniGraph(canvas, gx, y_mem - 1, gw, gh, mem_samples, mem_index,
                mem_count, 100, true);
  // NET Graph
  DrawMiniGraph(canvas, gx, y_net - 1, gw, gh, net_samples, net_index,
                net_count, 200, false);
}

///////////////////////////////////////////////////////
// ReadTempC(), ReadCPUPercent() and  ReadMemPercent()
// get acutal Temp, Mem and CPU
///////////////////////////////////////////////////////
static int ReadTempC() {
  std::ifstream f("/sys/class/thermal/thermal_zone0/temp");
  int milli = 0;
  if (!(f >> milli))
    return 0;
  return milli / 1000;
}
//////////////////////////////////////////////////////
static int ReadMemPercent() {
  std::ifstream f("/proc/meminfo");
  if (!f.is_open())
    return 0;

  std::string key;
  long long value = 0;
  std::string unit;

  long long mem_total = 0;
  long long mem_avail = 0;

  while (f >> key >> value >> unit) {
    if (key == "MemTotal:")
      mem_total = value;
    else if (key == "MemAvailable:") {
      mem_avail = value;
      break;
    }
  }

  if (mem_total <= 0)
    return 0;
  long long used = mem_total - mem_avail;
  int pct = (int)((used * 100) / mem_total);
  if (pct < 0)
    pct = 0;
  if (pct > 100)
    pct = 100;
  return pct;
}
///////////////////////////////////////////////////////
static int ReadCpuPercent() {
  // Returns 0..100, or -1 on error.
  // Uses delta between successive reads of /proc/stat (aggregate "cpu" line).

  static bool have_prev = false;
  static unsigned long long prev_total = 0;
  static unsigned long long prev_idle = 0;

  std::ifstream f("/proc/stat");
  if (!f.is_open())
    return -1;

  std::string cpu;
  unsigned long long user = 0, nice = 0, system = 0, idle = 0, iowait = 0,
                     irq = 0, softirq = 0, steal = 0;
  // Read: cpu  user nice system idle iowait irq softirq steal ...
  f >> cpu >> user >> nice >> system >> idle >> iowait >> irq >> softirq >>
      steal;
  if (cpu != "cpu")
    return -1;

  unsigned long long idle_all = idle + iowait;
  unsigned long long non_idle = user + nice + system + irq + softirq + steal;
  unsigned long long total = idle_all + non_idle;

  if (!have_prev) {
    have_prev = true;
    prev_total = total;
    prev_idle = idle_all;
    return 0; // first sample, no delta yet
  }

  unsigned long long totald = total - prev_total;
  unsigned long long idled = idle_all - prev_idle;

  prev_total = total;
  prev_idle = idle_all;

  if (totald == 0)
    return -1;

  double cpu_pct = (double)(totald - idled) * 100.0 / (double)totald;
  if (cpu_pct < 0)
    cpu_pct = 0;
  if (cpu_pct > 100)
    cpu_pct = 100;

  return (int)(cpu_pct + 0.5); // round to nearest int
}

///////////////////////////////////////////////////////
// Main
///////////////////////////////////////////////////////
int main(int argc, char *argv[]) {
  // Allow your existing flags (like --led-rows, --led-chain, etc.)
  rgb_matrix::RGBMatrix::Options matrix_options;
  rgb_matrix::RuntimeOptions runtime_options;

  // This helper is provided by rpi-rgb-led-matrix. Your globe.cc already uses
  // it.
  if (!rgb_matrix::ParseOptionsFromFlags(&argc, &argv, &matrix_options,
                                         &runtime_options)) {
    return 1;
  }

  // For sanity: we want a 6-face chain.
  const int face_w = matrix_options.cols;
  const int face_h = matrix_options.rows;
  const int faces = matrix_options.chain_length; // expect 6

  if (matrix_options.brightness <= 0)
    matrix_options.brightness = 40;
  int brightness_pct = matrix_options.brightness;

  status.hostname = GetHostname();

  // Fonts (load BEFORE CreateFromOptions in case privileges are dropped during
  // init)
  rgb_matrix::Font font_big;
  rgb_matrix::Font font_small;
  rgb_matrix::Font font_tiny;

  std::cerr << "Loading font: /home/jerry/rpi-rgb-led-matrix/fonts/7x13.bdf\n";
  if (!font_big.LoadFont("/home/jerry/rpi-rgb-led-matrix/fonts/7x13.bdf")) {
    std::cerr
        << "Couldn't load font /home/jerry/rpi-rgb-led-matrix/fonts/7x13.bdf\n";
    return 1;
  }
  std::cerr << "Loading font: /home/jerry/rpi-rgb-led-matrix/fonts/6x10.bdf\n";
  if (!font_small.LoadFont("/home/jerry/rpi-rgb-led-matrix/fonts/6x10.bdf")) {
    std::cerr
        << "Couldn't load font /home/jerry/rpi-rgb-led-matrix/fonts/6x10.bdf\n";
    return 1;
  }

  std::cerr << "Loading font: /home/jerry/rpi-rgb-led-matrix/fonts/5x8.bdf\n";
  if (!font_tiny.LoadFont("/home/jerry/rpi-rgb-led-matrix/fonts/5x8.bdf")) {
    std::cerr
        << "Couldn't load font /home/jerry/rpi-rgb-led-matrix/fonts/5x8.bdf\n";
    return 1;
  }

  RGBMatrix *matrix =
      RGBMatrix::CreateFromOptions(matrix_options, runtime_options);
  if (matrix == nullptr) {
    std::cerr << "Could not create RGBMatrix. Check wiring/options.\n";
    return 1;
  }

  std::signal(SIGTERM, InterruptHandler);
  std::signal(SIGINT, InterruptHandler);

  // Cards: what each face *would* display.
  // We rotate these assignments across faces.
  std::vector<std::string> card_titles = {"CLOCK",  "WEATHER", "FORECAST",
                                          "STATUS", "SYSTEM",  "MESSAGE"};

  std::vector<int> assign(faces);
  for (int i = 0; i < faces; i++)
    assign[i] = i % (int)card_titles.size();

  auto last_rotate = std::chrono::steady_clock::now();

  // Rotate Cube Faces every N Seconds
  int rotate = 15;
  const auto rotate_every = std::chrono::seconds(rotate);

  // ---- Weather state ----
  WeatherData weather;

  const std::string CACHE_PATH = "/var/tmp/cube_data/weather_cache.json";
  {
    WeatherData cached;
    if (LoadWeatherCache(CACHE_PATH, &cached)) {
      weather = cached;
      std::cerr << "Loaded weather cache\n";
      // Treat cached data as our initial "last known good" state so SYSTEM
      // doesn't show Wx DOWN at boot while displaying cached values.
      if (weather.ok) {
        have_weather_ok = true;
        last_weather_ok = std::chrono::steady_clock::now();
      }
    } else {
      std::cerr << "No weather cache\n";
    }
  }


// ---- Calendar state ----
const std::string CAL_CACHE_PATH = "/var/tmp/cube_data/calendar_cache.json";
const std::string CONFIG_PATH = "/home/jerry/InfoCube/config/infocube.conf";
const std::string CAL_ICS_URL = ReadConfigValue(CONFIG_PATH, "CAL_ICS_URL");
if (CAL_ICS_URL.empty()) {
  std::cerr << "Calendar: CAL_ICS_URL not set in " << CONFIG_PATH << "\n";
}

{
  CalendarEvent cached_cal;
  if (LoadCalendarCache(CAL_CACHE_PATH, &cached_cal)) {
    cal_event = cached_cal;
    std::cerr << "Loaded calendar cache\n";
    if (cal_event.ok) {
      have_calendar_ok = true;
      last_calendar_ok = std::chrono::steady_clock::now();
      status.calendar_ok = true;
      status.calendar_stale = false;
    }
  } else {
    std::cerr << "No calendar cache\n";
  }
}

const auto cal_fetch_every = std::chrono::minutes(10);
auto last_cal_fetch = std::chrono::steady_clock::now();

  const auto fetch_every = std::chrono::minutes(10);
  auto last_fetch = std::chrono::steady_clock::now();

  const double LAT = 30.5299299; // Marble Falls
  const double LON = -98.145384;
  const bool TEMP_F = true;

  bool first_fetch_pending = true;
  auto fetch_after = std::chrono::steady_clock::now() + std::chrono::seconds(3);
bool first_cal_fetch_pending = true;
auto cal_fetch_after = std::chrono::steady_clock::now() + std::chrono::seconds(4);

  FrameCanvas *canvas = matrix->CreateFrameCanvas();

  while (g_running) {
    // ---- Weather fetch timer ----
    auto now = std::chrono::steady_clock::now();

    // set variables to track to next face rotation event
    int rotate_progress =
        (int)std::chrono::duration_cast<std::chrono::seconds>(now - last_rotate)
            .count();
    if (rotate_progress < 0)
      rotate_progress = 0;
    if (rotate_progress > rotate)
      rotate_progress = rotate;
    int remaining = rotate - rotate_progress;
    if (remaining < 0)
      remaining = 0;

    // ---- Status metrics timer (run regardless of which face is visible) ----
    static auto last_status = std::chrono::steady_clock::now();
    if (now - last_status >= std::chrono::seconds(1)) {
      status.ip = GetIPAddress();
      status.uptime_str = GetUptimeString();
      int cpu = ReadCpuPercent(); // or your fake generator for testing
      if (cpu >= 0)
        status.cpu_pct = cpu;
      int net_kbps = ReadNetKBps();
      status.net_kbps = net_kbps;
      status.temp_c = ReadTempC();
      status.mem_pct = ReadMemPercent();

      // PushCpuSample(status.cpu_pct);
      PushSample(cpu_samples, cpu_index, cpu_count, status.cpu_pct);
      PushSample(temp_samples, temp_index, temp_count, status.temp_c);
      PushSample(mem_samples, mem_index, mem_count, status.mem_pct);
      PushSample(net_samples, net_index, net_count, net_kbps);
      last_status = now;
    }

    // ---- Weather fetch timer ----
    bool do_first_fetch = (first_fetch_pending && now >= fetch_after);
    bool do_periodic_fetch =
        (!first_fetch_pending && (now - last_fetch >= fetch_every));

    if (do_first_fetch || do_periodic_fetch) {
      WeatherData wd;

      if (FetchWeatherOpenMeteo(LAT, LON, TEMP_F, 6, &wd) && wd.ok) {
        weather = wd;
        SaveWeatherCache(CACHE_PATH, weather);
        std::cerr << "Saved weather cache\n";
        // Mark weather as OK + fresh
        have_weather_ok = true;
        last_weather_ok = now;
        status.weather_ok = true;
        status.weather_stale = false;

        std::cerr << "Weather: " << weather.temp << " " << weather.label
                  << " Hi " << weather.hi << " Lo " << weather.lo << "TMRW: Hi "
                  << weather.tomorrow_hi << " Lo " << weather.tomorrow_lo
                  << " Code " << weather.tomorrow_code << " PoP "
                  << weather.tomorrow_pop << "\n";
      } else {
        std::cerr << "Weather fetch failed\n";
        // Don't immediately clear OK unless we have never succeeded.
        // If we have a last-good time, we can go STALE instead.
        if (!have_weather_ok) {
          status.weather_ok = false;
          status.weather_stale = false;
        } else {
          status.weather_ok = true;
          status.weather_stale = (now - last_weather_ok) > WX_STALE_AFTER;
        }
      }

      last_fetch = now;
      first_fetch_pending = false; // first fetch has now been attempted
    }
    // Keep STALE up to date even between fetches
    if (have_weather_ok) {
      status.weather_ok = true;
      status.weather_stale = (now - last_weather_ok) > WX_STALE_AFTER;
    }


// ---- Calendar fetch timer ----
bool do_first_cal_fetch = (first_cal_fetch_pending && now >= cal_fetch_after);
bool do_periodic_cal_fetch =
    (!first_cal_fetch_pending && (now - last_cal_fetch >= cal_fetch_every));

if ((do_first_cal_fetch || do_periodic_cal_fetch) && !CAL_ICS_URL.empty()) {
  CalendarEvent ev;
  if (FetchCalendarICS(CAL_ICS_URL, &ev) && ev.ok) {
    cal_event = ev;
    SaveCalendarCache(CAL_CACHE_PATH, cal_event);
    std::cerr << "Saved calendar cache\n";
    have_calendar_ok = true;
    last_calendar_ok = now;
    status.calendar_ok = true;
    status.calendar_stale = false;
  } else {
    std::cerr << "Calendar fetch failed\n";
    if (!have_calendar_ok) {
      status.calendar_ok = false;
      status.calendar_stale = false;
    } else {
      status.calendar_ok = true;
      status.calendar_stale = (now - last_calendar_ok) > CAL_STALE_AFTER;
    }
  }
  last_cal_fetch = now;
  first_cal_fetch_pending = false;
}

// Keep STALE up to date even between fetches
if (have_calendar_ok) {
  status.calendar_ok = true;
  status.calendar_stale = (now - last_calendar_ok) > CAL_STALE_AFTER;
}

    if (now - last_rotate >= rotate_every) {
      // rotate assignments right by 1 (card content moves to next face)
      int last = assign.back();
      for (int i = faces - 1; i > 0; i--) {
        assign[i] = assign[i - 1];
      }
      assign[0] = last;
      last_rotate = now;
    }
    canvas->Clear();

    const std::string time_str = NowTimeString();
    const std::string date_str = NowDateString();

    for (int face = 0; face < faces; face++) {
      int ox = face * face_w;
      int oy = 0;

      const std::string title = card_titles[assign[face]];

      DrawFaceFrameAndTitle(canvas, font_small, ox, oy, face_w, face_h, title,
                            weather);

      if (title == "CLOCK") {
        // existing clock draw function later, for now keep your current clock
        // block here
        int x_time = ox + CenterTextX(font_big, time_str, face_w);
        rgb_matrix::DrawText(canvas, font_big, x_time, oy + 36,
                             rgb_matrix::Color(255, 255, 255), nullptr,
                             time_str.c_str(), 0);

        int x_date = ox + CenterTextX(font_small, date_str, face_w);
        rgb_matrix::DrawText(canvas, font_small, x_date, oy + 52,
                             rgb_matrix::Color(160, 160, 160), nullptr,
                             date_str.c_str(), 0);

        int sec = NowSecond();
        DrawProgressBar(canvas, ox + 6, oy + 56, face_w - 12, 6, remaining,
                        rotate);
        // DrawProgressBar(canvas, ox + 6, oy + 56, face_w - 12, 6, sec, 60);
      } else if (title == "WEATHER") {
        if (weather.ok)
          DrawFaceWeather(canvas, font_big, font_small, font_tiny, ox, oy,
                          face_w, weather);
        else {
          // Default: time/date + debug label
          int x_time = ox + CenterTextX(font_big, time_str, face_w);
          rgb_matrix::DrawText(canvas, font_big, x_time, oy + 36,
                               rgb_matrix::Color(255, 255, 255), nullptr,
                               time_str.c_str(), 0);
          DrawFaceDebugLabel(canvas, font_small, ox, oy, face);
        }
      } else if (title == "FORECAST") {
        if (weather.ok)
          DrawFaceForecast(canvas, font_small, font_tiny, ox, oy, face_w,
                           weather);
        else {
          // Default: time/date + debug label
          int x_time = ox + CenterTextX(font_big, time_str, face_w);
          rgb_matrix::DrawText(canvas, font_big, x_time, oy + 36,
                               rgb_matrix::Color(255, 255, 255), nullptr,
                               time_str.c_str(), 0);
          DrawFaceDebugLabel(canvas, font_small, ox, oy, face);
        }
      } else if (title == "MESSAGE") {
        DrawFaceMessage(canvas, font_small, font_tiny, ox, oy, face_w, face_h,
                       cal_event, status);
      } else if (title == "STATUS") {
        DrawFaceStatus(canvas, font_small, font_tiny, ox, oy, face_w, face_h);
      } else if (title == "SYSTEM") {
        DrawFaceSystem(canvas, font_small, font_tiny, ox, oy, face_w, face_h,
                       status, rotate, brightness_pct, "v1.30", now);
      }
    } // end for
    canvas = matrix->SwapOnVSync(canvas);

    // ~30 FPS
    std::this_thread::sleep_for(std::chrono::milliseconds(33));
  } // end while
  delete matrix;
  return 0;
} // end main