#include <Arduino.h>
#include <Wire.h>
#include <Preferences.h>
#include <WiFi.h>
#include <HTTPClient.h>
#include <WiFiClientSecure.h>
#include <vector>
#include <Arduino_GFX_Library.h>
#include <TouchDrvCSTXXX.hpp>
#include <U8g2lib.h>
#include <mbedtls/base64.h>
#include <sodium.h>

extern "C" {
#include "../../official-demo/examples/Arduino-v3.3.5/libraries/lvgl/src/extra/libs/qrcode/qrcodegen.h"
}

#define PIN_LCD_CS    12
#define PIN_LCD_SCLK  38
#define PIN_LCD_D0     4
#define PIN_LCD_D1     5
#define PIN_LCD_D2     6
#define PIN_LCD_D3     7
#define PIN_LCD_RST    2
#define PIN_I2C_SDA   15
#define PIN_I2C_SCL   14
#define PIN_TP_INT    11

#define DISP_W 480
#define DISP_H 480

#define C_BG      0x0841u
#define C_PANEL   0x1082u
#define C_CARD    0x18C3u
#define C_BORDER  0x39C7u
#define C_TEXT    0xFFFFu
#define C_MUTE    0xBDF7u
#define C_ACCENT  0x5D5Fu
#define C_OK      0x3666u
#define C_WARN    0xECA0u
#define C_BAD     0xD145u
#define C_BUTTON  0x2145u
#define C_BUTTON2 0x3186u

#define FONT_HEAD u8g2_font_10x20_t_cyrillic
#define FONT_BODY u8g2_font_9x15_t_cyrillic
#define FONT_SMALL u8g2_font_6x13_t_cyrillic

Arduino_DataBus *gBus = new Arduino_ESP32QSPI(
    PIN_LCD_CS, PIN_LCD_SCLK, PIN_LCD_D0, PIN_LCD_D1, PIN_LCD_D2, PIN_LCD_D3);
Arduino_CO5300 *gfx = new Arduino_CO5300(
    gBus, PIN_LCD_RST, 0, DISP_W, DISP_H, 0, 0, 0, 0);

TouchDrvCST92xx gTouch;
Preferences gPrefs;

enum ScreenId {
  SCR_LOCK,
  SCR_HOME,
  SCR_STATUS,
  SCR_CONNECTION,
  SCR_WIFI_EDIT,
  SCR_SERVERS,
  SCR_ACCOUNT,
  SCR_WALLET,
  SCR_WALLET_QR,
  SCR_REQUESTS,
  SCR_REQUEST_DETAIL,
  SCR_SETTINGS,
  SCR_PIN_EDIT,
  SCR_TEXT_INPUT,
  SCR_CONFIRM
};

enum ActionId {
  ACT_NONE = 0,
  ACT_HOME,
  ACT_BACK,
  ACT_STATUS,
  ACT_CONNECTION,
  ACT_ACCOUNT,
  ACT_WALLET,
  ACT_REQUESTS,
  ACT_SETTINGS,
  ACT_REFRESH,
  ACT_WIFI,
  ACT_SERVERS,
  ACT_CONNECT,
  ACT_DISCONNECT,
  ACT_EDIT_SSID,
  ACT_EDIT_WIFI_PASSWORD,
  ACT_VERIFY_WIFI,
  ACT_RESET_WIFI,
  ACT_EDIT_API,
  ACT_EDIT_RPC,
  ACT_EDIT_WS,
  ACT_VERIFY_SERVERS,
  ACT_SET_TEST_SERVERS,
  ACT_EDIT_LOGIN,
  ACT_EDIT_HOMESERVER,
  ACT_GENERATE_SECRET,
  ACT_CLEAR_ACCOUNT,
  ACT_SHOW_QR,
  ACT_BALANCE_PLUS_010,
  ACT_BALANCE_PLUS_025,
  ACT_BALANCE_MINUS_010,
  ACT_CHECK_BALANCE,
  ACT_REGISTER,
  ACT_OPEN_REQ_0,
  ACT_OPEN_REQ_1,
  ACT_APPROVE_REQUEST,
  ACT_REJECT_REQUEST,
  ACT_CHANGE_PIN,
  ACT_RESET_ONLINE,
  ACT_FULL_RESET,
  ACT_CONFIRM_YES,
  ACT_CONFIRM_NO,
  ACT_DIGIT_0,
  ACT_DIGIT_1,
  ACT_DIGIT_2,
  ACT_DIGIT_3,
  ACT_DIGIT_4,
  ACT_DIGIT_5,
  ACT_DIGIT_6,
  ACT_DIGIT_7,
  ACT_DIGIT_8,
  ACT_DIGIT_9,
  ACT_INPUT_BACKSPACE,
  ACT_INPUT_OK,
  ACT_INPUT_CANCEL,
  ACT_KB_SHIFT,
  ACT_KB_SPACE
};

enum EditTarget {
  EDIT_NONE = 0,
  EDIT_SSID,
  EDIT_WIFI_PASSWORD,
  EDIT_LOGIN,
  EDIT_HOMESERVER,
  EDIT_API,
  EDIT_RPC,
  EDIT_WS,
  EDIT_NEW_PIN,
  EDIT_UNLOCK_PIN
};

enum ConfirmTarget {
  CONFIRM_NONE = 0,
  CONFIRM_REGISTER,
  CONFIRM_CLEAR_ACCOUNT,
  CONFIRM_FULL_RESET
};

struct Button {
  int16_t x;
  int16_t y;
  int16_t w;
  int16_t h;
  ActionId action;
  bool enabled;
  char label[40];
};

struct RequestItem {
  String type;
  String actor;
  String details;
  String status;
};

struct AppData {
  String pin;
  String wifiSsid;
  String wifiPassword;
  String login;
  String homeserverName;
  String secret;
  String walletAddress;
  String userPdaAddress;
  String registrationSignature;
  String apiUrl;
  String rpcUrl;
  String wsUrl;
  uint16_t balanceCentiSol;
  bool wifiReady;
  bool serversReady;
  bool secretReady;
  bool registered;
  bool online;
};

struct RuntimeNetState {
  String wifiStatus;
  String localIp;
  String apiStatus;
  String rpcStatus;
  String wsStatus;
  String serverVersion;
  int32_t rssi;
};

static ScreenId gScreen = SCR_HOME;
static ScreenId gPrevScreen = SCR_HOME;
static EditTarget gEditTarget = EDIT_NONE;
static ConfirmTarget gConfirmTarget = CONFIRM_NONE;
static AppData gData;
static Button gButtons[32];
static uint8_t gButtonCount = 0;
static RequestItem gRequests[2];
static int gCurrentRequest = 0;
static String gNotice;
static String gEditBuffer;
static String gLockBuffer;
static RuntimeNetState gNetState;
static bool gKeyboardAlt = false;
static bool gNeedRedraw = true;
static bool gTouchDown = false;
static int16_t gTouchStartX = 0;
static int16_t gTouchStartY = 0;
static int16_t gTouchLastX = 0;
static int16_t gTouchLastY = 0;

struct DerivedKeyState {
  bool ready;
  uint8_t masterSecret[32];
  uint8_t rootPub[32];
  uint8_t rootSk[64];
  uint8_t blockchainPub[32];
  uint8_t blockchainSk[64];
  uint8_t devicePub[32];
  uint8_t deviceSk[64];
};

static DerivedKeyState gDerivedKeys = {};

static const char *kSystemProgramId = "11111111111111111111111111111111";
static const char *kEd25519ProgramId = "Ed25519SigVerify111111111111111111111111111";
static const char *kSysvarInstructionsId = "Sysvar1nstructions1111111111111111111111111";
static const char *kShineUsersProgramId = "FZS1YctoeEhCkZ5VTjsysUFAXR8CqxYztcLboXcg2Rpm";
static const char *kShineLoginGuardProgramId = "3xkopA7cXagxzMFrKdv3NCBfV6BKiRJCk69kr27M2sRo";
static const char *kShinePaymentsProgramId = "c4yTa4JT9EtQDCBX9LmWFK6T2gp4JGsuymFbom2EudW";
static const char *kUsersSeedPrefix = "user_login=";
static const char *kUsersEconomyConfigSeed = "shine_users_economy_config";
static const char *kPaymentsInflowSeed = "shine_payments_inflow_vault";
static const char *kProgramDerivedAddressMarker = "ProgramDerivedAddress";
static const char *kLastBlockPrefix = "SHiNE_LAST_BLOCK";
static const uint8_t kBlockTypeRootKey = 1;
static const uint8_t kBlockTypeDeviceKey = 2;
static const uint8_t kBlockTypeBlockchainRegistry = 3;
static const uint8_t kBlockTypeServerProfile = 30;
static const uint8_t kBlockTypeAccessServers = 40;
static const uint8_t kBlockTypeSessions = 50;
static const uint8_t kBlockTypeTrustedState = 70;
static const bool kPinLockEnabled = false;

static const char *KB_ALPHA[4] = {
  "qwertyuiop",
  "asdfghjkl",
  "zxcvbnm",
  ""
};

static const char *KB_ALT[4] = {
  "1234567890",
  "-_.:@/+?&=",
  "[]()%#,;!*",
  ""
};

static void clearButtons() {
  gButtonCount = 0;
}

static int utf8Len(const String &s) {
  int count = 0;
  for (size_t i = 0; i < s.length(); i++) {
    uint8_t c = (uint8_t)s[i];
    if ((c & 0xC0) != 0x80) {
      count++;
    }
  }
  return count;
}

static String trimCopy(const String &value) {
  String copy = value;
  copy.trim();
  return copy;
}

static bool startsWithAny(const String &value, const char *a, const char *b) {
  return value.startsWith(a) || value.startsWith(b);
}

static String normalizeLogin(const String &value) {
  String out = trimCopy(value);
  out.toLowerCase();
  return out;
}

static void appendAsciiChar(String &out, char c) {
  if (c == '\n' || c == '\r' || c == '\t') {
    out += c;
    return;
  }
  uint8_t uc = (uint8_t)c;
  if (uc >= 32 && uc <= 126) {
    out += c;
    return;
  }
  out += '?';
}

static void appendTranslit(String &out, const char *value) {
  out += value;
}

static void appendUtf8Translit(String &out, uint8_t first, uint8_t second) {
  if (first == 0xD0) {
    switch (second) {
      case 0x81: appendTranslit(out, "Yo"); return;
      case 0x90: appendTranslit(out, "A"); return;
      case 0x91: appendTranslit(out, "B"); return;
      case 0x92: appendTranslit(out, "V"); return;
      case 0x93: appendTranslit(out, "G"); return;
      case 0x94: appendTranslit(out, "D"); return;
      case 0x95: appendTranslit(out, "E"); return;
      case 0x96: appendTranslit(out, "Zh"); return;
      case 0x97: appendTranslit(out, "Z"); return;
      case 0x98: appendTranslit(out, "I"); return;
      case 0x99: appendTranslit(out, "Y"); return;
      case 0x9A: appendTranslit(out, "K"); return;
      case 0x9B: appendTranslit(out, "L"); return;
      case 0x9C: appendTranslit(out, "M"); return;
      case 0x9D: appendTranslit(out, "N"); return;
      case 0x9E: appendTranslit(out, "O"); return;
      case 0x9F: appendTranslit(out, "P"); return;
      case 0xA0: appendTranslit(out, "R"); return;
      case 0xA1: appendTranslit(out, "S"); return;
      case 0xA2: appendTranslit(out, "T"); return;
      case 0xA3: appendTranslit(out, "U"); return;
      case 0xA4: appendTranslit(out, "F"); return;
      case 0xA5: appendTranslit(out, "Kh"); return;
      case 0xA6: appendTranslit(out, "Ts"); return;
      case 0xA7: appendTranslit(out, "Ch"); return;
      case 0xA8: appendTranslit(out, "Sh"); return;
      case 0xA9: appendTranslit(out, "Sch"); return;
      case 0xAA: appendTranslit(out, ""); return;
      case 0xAB: appendTranslit(out, "Y"); return;
      case 0xAC: appendTranslit(out, ""); return;
      case 0xAD: appendTranslit(out, "E"); return;
      case 0xAE: appendTranslit(out, "Yu"); return;
      case 0xAF: appendTranslit(out, "Ya"); return;
      case 0xB0: appendTranslit(out, "a"); return;
      case 0xB1: appendTranslit(out, "b"); return;
      case 0xB2: appendTranslit(out, "v"); return;
      case 0xB3: appendTranslit(out, "g"); return;
      case 0xB4: appendTranslit(out, "d"); return;
      case 0xB5: appendTranslit(out, "e"); return;
      case 0xB6: appendTranslit(out, "zh"); return;
      case 0xB7: appendTranslit(out, "z"); return;
      case 0xB8: appendTranslit(out, "i"); return;
      case 0xB9: appendTranslit(out, "y"); return;
      case 0xBA: appendTranslit(out, "k"); return;
      case 0xBB: appendTranslit(out, "l"); return;
      case 0xBC: appendTranslit(out, "m"); return;
      case 0xBD: appendTranslit(out, "n"); return;
      case 0xBE: appendTranslit(out, "o"); return;
      case 0xBF: appendTranslit(out, "p"); return;
      default: break;
    }
  } else if (first == 0xD1) {
    switch (second) {
      case 0x80: appendTranslit(out, "r"); return;
      case 0x81: appendTranslit(out, "s"); return;
      case 0x82: appendTranslit(out, "t"); return;
      case 0x83: appendTranslit(out, "u"); return;
      case 0x84: appendTranslit(out, "f"); return;
      case 0x85: appendTranslit(out, "kh"); return;
      case 0x86: appendTranslit(out, "ts"); return;
      case 0x87: appendTranslit(out, "ch"); return;
      case 0x88: appendTranslit(out, "sh"); return;
      case 0x89: appendTranslit(out, "sch"); return;
      case 0x8A: appendTranslit(out, ""); return;
      case 0x8B: appendTranslit(out, "y"); return;
      case 0x8C: appendTranslit(out, ""); return;
      case 0x8D: appendTranslit(out, "e"); return;
      case 0x8E: appendTranslit(out, "yu"); return;
      case 0x8F: appendTranslit(out, "ya"); return;
      case 0x91: appendTranslit(out, "yo"); return;
      default: break;
    }
  }
  out += '?';
}

static String toDisplayAscii(const String &text) {
  String out;
  out.reserve(text.length() * 2);
  for (size_t i = 0; i < text.length(); i++) {
    uint8_t c = (uint8_t)text[i];
    if (c < 0x80) {
      appendAsciiChar(out, (char)c);
      continue;
    }
    if ((c == 0xD0 || c == 0xD1) && i + 1 < text.length()) {
      appendUtf8Translit(out, c, (uint8_t)text[i + 1]);
      i++;
      continue;
    }
    out += '?';
  }
  return out;
}

static uint8_t fontScale(const uint8_t *font) {
  if (font == (const uint8_t *)FONT_HEAD) {
    return 2;
  }
  if (font == (const uint8_t *)FONT_BODY) {
    return 2;
  }
  return 1;
}

static int defaultFontCharWidth(uint8_t scale) {
  return 6 * scale;
}

static int defaultFontHeight(uint8_t scale) {
  return 8 * scale;
}

static void setFont(const uint8_t *font) {
  gfx->setFont();
  gfx->setTextSize(fontScale(font));
}

static void drawText(int x, int y, const String &text, uint16_t color, const uint8_t *font = (const uint8_t *)FONT_BODY) {
  String display = toDisplayAscii(text);
  setFont(font);
  gfx->setTextColor(color);
  gfx->setCursor(x, y);
  gfx->print(display);
}

static void drawTextCentered(int x, int y, int w, int h, const String &text, uint16_t color, const uint8_t *font = (const uint8_t *)FONT_BODY) {
  String display = toDisplayAscii(text);
  uint8_t scale = fontScale(font);
  setFont(font);
  gfx->setTextColor(color);
  int textW = display.length() * defaultFontCharWidth(scale);
  int textH = defaultFontHeight(scale);
  int textX = x + (w - textW) / 2;
  int textY = y + (h + textH) / 2 - 2;
  if (textX < x + 6) {
    textX = x + 6;
  }
  gfx->setCursor(textX, textY);
  gfx->print(display);
}

static void drawWrappedText(int x, int y, int maxChars, int lineHeight, const String &text, uint16_t color, const uint8_t *font = (const uint8_t *)FONT_BODY) {
  String display = toDisplayAscii(text);
  uint8_t scale = fontScale(font);
  int effectiveLineHeight = lineHeight;
  int minLineHeight = defaultFontHeight(scale) + 4;
  if (effectiveLineHeight < minLineHeight) {
    effectiveLineHeight = minLineHeight;
  }
  setFont(font);
  gfx->setTextColor(color);
  String line;
  int lineY = y;
  for (size_t i = 0; i < display.length(); i++) {
    char c = display[i];
    if (c == '\n') {
      gfx->setCursor(x, lineY);
      gfx->print(line);
      line = "";
      lineY += effectiveLineHeight;
      continue;
    }
    line += c;
    if (utf8Len(line) >= maxChars && c == ' ') {
      gfx->setCursor(x, lineY);
      gfx->print(line);
      line = "";
      lineY += effectiveLineHeight;
    }
  }
  if (line.length() > 0) {
    gfx->setCursor(x, lineY);
    gfx->print(line);
  }
}

static void drawPanel(int x, int y, int w, int h, uint16_t fill = C_PANEL, uint16_t border = C_BORDER, int radius = 10) {
  gfx->fillRoundRect(x, y, w, h, radius, fill);
  gfx->drawRoundRect(x, y, w, h, radius, border);
}

static void addButton(int x, int y, int w, int h, ActionId action, const String &label, bool enabled = true, uint16_t fill = C_BUTTON) {
  if (gButtonCount >= 32) {
    return;
  }
  Button &btn = gButtons[gButtonCount++];
  btn.x = x;
  btn.y = y;
  btn.w = w;
  btn.h = h;
  btn.action = action;
  btn.enabled = enabled;
  memset(btn.label, 0, sizeof(btn.label));
  label.substring(0, sizeof(btn.label) - 1).toCharArray(btn.label, sizeof(btn.label));
  uint16_t fillColor = enabled ? fill : C_CARD;
  drawPanel(x, y, w, h, fillColor, enabled ? C_BORDER : C_BORDER, 12);
  drawTextCentered(x, y, w, h, label, enabled ? C_TEXT : C_MUTE, (const uint8_t *)FONT_BODY);
}

static void drawStatusChip(int x, int y, int w, const String &label, bool ok) {
  uint16_t color = ok ? C_OK : C_BAD;
  drawPanel(x, y, w, 28, color, color, 8);
  drawText(x + 8, y + 19, label, C_TEXT, (const uint8_t *)FONT_SMALL);
}

static String shortenValue(const String &value, int keepStart = 6, int keepEnd = 4) {
  if ((int)value.length() <= keepStart + keepEnd + 3) {
    return value;
  }
  return value.substring(0, keepStart) + "..." + value.substring(value.length() - keepEnd);
}

static String repeatChar(char c, int count) {
  String out;
  for (int i = 0; i < count; i++) {
    out += c;
  }
  return out;
}

static bool canRegister() {
  return gData.wifiReady &&
         gData.serversReady &&
         isValidLoginValue(gData.login) &&
         gData.secretReady &&
         gData.balanceCentiSol >= 20 &&
         !gData.registered;
}

static String registrationSummary() {
  if (gData.registered) {
    return "Homeserver активен";
  }
  if (!gData.wifiReady) {
    return "Нужен Wi-Fi";
  }
  if (!gData.serversReady) {
    return "Нужны серверы";
  }
  if (gData.login.length() == 0) {
    return "Нужен логин";
  }
  if (!isValidLoginValue(gData.login)) {
    return "Логин невалиден";
  }
  if (!gData.secretReady) {
    return "Нужен секрет";
  }
  if (gData.balanceCentiSol < 20) {
    return "Нужно >= 0.20 SOL";
  }
  return "Можно регистрировать";
}

static String registrationDetailsShort() {
  if (gData.userPdaAddress.length() > 0) {
    return shortenValue(gData.userPdaAddress, 8, 6);
  }
  if (gData.registrationSignature.length() > 0) {
    return shortenValue(gData.registrationSignature, 8, 6);
  }
  return "ещё не отправлялась";
}

static String boolText(bool value, const String &yesText = "Да", const String &noText = "Нет") {
  return value ? yesText : noText;
}

static String balanceText() {
  char buf[32];
  snprintf(buf, sizeof(buf), "%u.%02u SOL", gData.balanceCentiSol / 100, gData.balanceCentiSol % 100);
  return String(buf);
}

static void drawTopBar(const String &title) {
  gfx->fillScreen(C_BG);
  drawText(20, 26, title, C_TEXT, (const uint8_t *)FONT_HEAD);
  drawStatusChip(20, 38, 82, gData.wifiReady ? "Wi-Fi OK" : "Wi-Fi нет", gData.wifiReady);
  drawStatusChip(110, 38, 82, gData.serversReady ? "RPC OK" : "RPC нет", gData.serversReady);
  drawStatusChip(200, 38, 92, gData.registered ? "Рег OK" : "Не рег", gData.registered);
  drawStatusChip(300, 38, 92, gData.online ? "Онлайн" : "Офлайн", gData.online);
  drawStatusChip(400, 38, 60, "PIN", true);
  gfx->drawFastHLine(20, 74, 440, C_BORDER);
}

static void drawNoticeBox() {
  if (gNotice.length() == 0) {
    return;
  }
  const int noticeX = 20;
  const int noticeW = 440;
  const int noticeH = 42;
  int noticeY = 420;
  for (uint8_t i = 0; i < gButtonCount; i++) {
    const Button &btn = gButtons[i];
    bool overlapX = btn.x < (noticeX + noticeW) && (btn.x + btn.w) > noticeX;
    bool overlapY = btn.y < (noticeY + noticeH) && (btn.y + btn.h) > noticeY;
    if (overlapX && overlapY) {
      int candidateY = btn.y - noticeH - 6;
      if (candidateY < noticeY) {
        noticeY = candidateY;
      }
    }
  }
  if (noticeY < 82) {
    noticeY = 82;
  }
  drawPanel(noticeX, noticeY, noticeW, noticeH, C_CARD, C_BORDER, 10);
  drawWrappedText(noticeX + 12, noticeY + 24, 52, 16, gNotice, C_MUTE, (const uint8_t *)FONT_SMALL);
}

static const char *base58Alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";

static String bytesToBase58(const uint8_t *data, size_t len) {
  uint8_t tmp[80] = {};
  int tmpLen = 0;
  for (size_t i = 0; i < len; i++) {
    int carry = data[i];
    for (int j = 0; j < tmpLen; j++) {
      carry += tmp[j] << 8;
      tmp[j] = carry % 58;
      carry /= 58;
    }
    while (carry > 0) {
      tmp[tmpLen++] = carry % 58;
      carry /= 58;
    }
  }
  int zeroes = 0;
  while (zeroes < (int)len && data[zeroes] == 0) {
    zeroes++;
  }
  String out;
  for (int i = 0; i < zeroes; i++) {
    out += '1';
  }
  for (int i = tmpLen - 1; i >= 0; i--) {
    out += base58Alphabet[tmp[i]];
  }
  return out;
}

static bool base58ToBytes(const String &value, std::vector<uint8_t> &out) {
  out.clear();
  String clean = trimCopy(value);
  if (clean.length() == 0) {
    return true;
  }
  std::vector<uint8_t> digits;
  digits.reserve(clean.length());
  for (size_t i = 0; i < clean.length(); i++) {
    char c = clean[i];
    const char *ptr = strchr(base58Alphabet, c);
    if (!ptr) {
      return false;
    }
    int carry = (int)(ptr - base58Alphabet);
    for (size_t j = 0; j < digits.size(); j++) {
      int valueAcc = digits[j] * 58 + carry;
      digits[j] = (uint8_t)(valueAcc & 0xFF);
      carry = valueAcc >> 8;
    }
    while (carry > 0) {
      digits.push_back((uint8_t)(carry & 0xFF));
      carry >>= 8;
    }
  }
  int zeroes = 0;
  while (zeroes < clean.length() && clean[zeroes] == '1') {
    zeroes++;
  }
  out.assign(zeroes, 0);
  for (int i = (int)digits.size() - 1; i >= 0; i--) {
    out.push_back(digits[(size_t)i]);
  }
  return true;
}

static String randomBase58(size_t byteCount) {
  uint8_t data[64];
  if (byteCount > sizeof(data)) {
    byteCount = sizeof(data);
  }
  for (size_t i = 0; i < byteCount; i++) {
    data[i] = (uint8_t)esp_random();
  }
  return bytesToBase58(data, byteCount);
}

static String base64Encode(const uint8_t *data, size_t len) {
  size_t outLen = 0;
  size_t outSize = ((len + 2) / 3) * 4 + 4;
  std::vector<uint8_t> out(outSize);
  if (mbedtls_base64_encode(out.data(), out.size(), &outLen, data, len) != 0) {
    return "";
  }
  return String((const char *)out.data()).substring(0, outLen);
}

static bool sha256Raw(const uint8_t *data, size_t len, uint8_t out32[32]) {
  return crypto_hash_sha256(out32, data, (unsigned long long)len) == 0;
}

static bool sha256String(const String &text, uint8_t out32[32]) {
  return sha256Raw((const uint8_t *)text.c_str(), text.length(), out32);
}

static bool isValidLoginValue(const String &login) {
  if (login.length() == 0 || login.length() > 20) {
    return false;
  }
  for (size_t i = 0; i < login.length(); i++) {
    char c = login[i];
    bool ok = (c >= 'a' && c <= 'z') ||
              (c >= '0' && c <= '9') ||
              c == '_';
    if (!ok) {
      return false;
    }
  }
  return true;
}

static void shortVecEncode(size_t value, std::vector<uint8_t> &out) {
  while (true) {
    uint8_t elem = (uint8_t)(value & 0x7F);
    value >>= 7;
    if (value == 0) {
      out.push_back(elem);
      return;
    }
    out.push_back(elem | 0x80);
  }
}

static void pushU32LE(std::vector<uint8_t> &out, uint32_t value) {
  out.push_back((uint8_t)(value & 0xFF));
  out.push_back((uint8_t)((value >> 8) & 0xFF));
  out.push_back((uint8_t)((value >> 16) & 0xFF));
  out.push_back((uint8_t)((value >> 24) & 0xFF));
}

static void pushU64LE(std::vector<uint8_t> &out, uint64_t value) {
  for (int i = 0; i < 8; i++) {
    out.push_back((uint8_t)((value >> (8 * i)) & 0xFF));
  }
}

static void pushStrU8(std::vector<uint8_t> &out, const String &value) {
  out.push_back((uint8_t)value.length());
  for (size_t i = 0; i < value.length(); i++) {
    out.push_back((uint8_t)value[i]);
  }
}

static void pushFixed(std::vector<uint8_t> &out, const uint8_t *data, size_t len) {
  out.insert(out.end(), data, data + len);
}

static bool deriveKeysFromMasterSecret(const uint8_t masterSecret[32]) {
  memset(&gDerivedKeys, 0, sizeof(gDerivedKeys));
  memcpy(gDerivedKeys.masterSecret, masterSecret, 32);
  String secretB64 = base64Encode(masterSecret, 32);
  if (secretB64.length() == 0) {
    return false;
  }
  const char *suffixes[3] = {"root.key", "bch.key", "dev.key"};
  uint8_t *pubs[3] = {gDerivedKeys.rootPub, gDerivedKeys.blockchainPub, gDerivedKeys.devicePub};
  uint8_t *sks[3] = {gDerivedKeys.rootSk, gDerivedKeys.blockchainSk, gDerivedKeys.deviceSk};
  for (int i = 0; i < 3; i++) {
    String material = secretB64 + "|" + suffixes[i];
    uint8_t seed[32];
    if (!sha256String(material, seed)) {
      return false;
    }
    if (crypto_sign_seed_keypair(pubs[i], sks[i], seed) != 0) {
      return false;
    }
  }
  gDerivedKeys.ready = true;
  return true;
}

static bool restoreDerivedKeysFromSecret() {
  gDerivedKeys.ready = false;
  if (gData.secret.length() == 0) {
    gData.secretReady = false;
    gData.walletAddress = "";
    return true;
  }
  std::vector<uint8_t> secretBytes;
  if (!base58ToBytes(gData.secret, secretBytes) || secretBytes.size() != 32) {
    gData.secretReady = false;
    gData.walletAddress = "";
    return false;
  }
  if (!deriveKeysFromMasterSecret(secretBytes.data())) {
    gData.secretReady = false;
    gData.walletAddress = "";
    return false;
  }
  gData.secretReady = true;
  gData.walletAddress = bytesToBase58(gDerivedKeys.devicePub, 32);
  return true;
}

static bool deriveFreshSecretAndWallet() {
  uint8_t secret[32];
  for (size_t i = 0; i < sizeof(secret); i++) {
    secret[i] = (uint8_t)esp_random();
  }
  if (!deriveKeysFromMasterSecret(secret)) {
    return false;
  }
  gData.secret = bytesToBase58(secret, sizeof(secret));
  gData.walletAddress = bytesToBase58(gDerivedKeys.devicePub, 32);
  gData.secretReady = true;
  return true;
}

static bool base58ToFixed32(const char *value, uint8_t out[32]) {
  std::vector<uint8_t> raw;
  if (!base58ToBytes(String(value), raw) || raw.size() != 32) {
    return false;
  }
  memcpy(out, raw.data(), 32);
  return true;
}

static bool findProgramAddress(const std::vector<std::vector<uint8_t>> &seeds, const char *programIdB58, uint8_t out[32]) {
  uint8_t programId[32];
  if (!base58ToFixed32(programIdB58, programId)) {
    return false;
  }
  for (int bump = 255; bump >= 0; bump--) {
    crypto_hash_sha256_state st;
    crypto_hash_sha256_init(&st);
    for (const auto &seed : seeds) {
      crypto_hash_sha256_update(&st, seed.data(), (unsigned long long)seed.size());
    }
    uint8_t bumpByte = (uint8_t)bump;
    crypto_hash_sha256_update(&st, &bumpByte, 1);
    crypto_hash_sha256_update(&st, programId, 32);
    crypto_hash_sha256_update(&st, (const unsigned char *)kProgramDerivedAddressMarker, strlen(kProgramDerivedAddressMarker));
    crypto_hash_sha256_final(&st, out);
    if (crypto_core_ed25519_is_valid_point(out) == 0) {
      return true;
    }
  }
  return false;
}

static std::vector<uint8_t> buildLastBlockStateBytes(const String &login, const String &blockchainName) {
  std::vector<uint8_t> out;
  out.reserve(80);
  pushFixed(out, (const uint8_t *)kLastBlockPrefix, strlen(kLastBlockPrefix));
  pushStrU8(out, login);
  pushStrU8(out, blockchainName);
  pushU32LE(out, 0);
  out.insert(out.end(), 32, 0);
  pushU64LE(out, 0);
  return out;
}

static std::vector<uint8_t> buildUnsignedCreateRecord(
    const String &login,
    const String &blockchainName,
    const String &serverAddress,
    const uint8_t rootPub[32],
    const uint8_t devicePub[32],
    const uint8_t blockchainPub[32],
    const uint8_t lastBlockSignature[64],
    uint64_t createdAtMs) {
  std::vector<uint8_t> out;
  out.reserve(512);
  pushFixed(out, (const uint8_t *)"SHiNE", 5);
  out.push_back(1);
  out.push_back(0);
  out.push_back(0);
  out.push_back(0);
  pushU64LE(out, createdAtMs);
  pushU64LE(out, createdAtMs);
  pushU32LE(out, 0);
  out.insert(out.end(), 32, 0);
  pushStrU8(out, login);
  out.push_back(7);

  out.push_back(kBlockTypeRootKey);
  out.push_back(0);
  pushFixed(out, rootPub, 32);

  out.push_back(kBlockTypeDeviceKey);
  out.push_back(0);
  pushFixed(out, devicePub, 32);

  out.push_back(kBlockTypeBlockchainRegistry);
  out.push_back(0);
  out.push_back(1);
  out.push_back(1);
  pushStrU8(out, blockchainName);
  pushFixed(out, blockchainPub, 32);
  pushU64LE(out, 100000);
  pushU64LE(out, 0);
  pushU32LE(out, 0);
  out.insert(out.end(), 32, 0);
  pushFixed(out, lastBlockSignature, 64);
  out.push_back(0);

  out.push_back(kBlockTypeServerProfile);
  out.push_back(0);
  out.push_back(1);
  out.push_back(1);
  out.push_back(0);
  pushStrU8(out, serverAddress);
  out.push_back(0);

  out.push_back(kBlockTypeAccessServers);
  out.push_back(0);
  out.push_back(0);

  out.push_back(kBlockTypeSessions);
  out.push_back(0);
  out.push_back(1);
  out.push_back(0);

  out.push_back(kBlockTypeTrustedState);
  out.push_back(0);
  out.push_back(0);

  uint16_t recordLen = (uint16_t)(out.size() + 64);
  out[7] = (uint8_t)(recordLen & 0xFF);
  out[8] = (uint8_t)((recordLen >> 8) & 0xFF);
  return out;
}

static std::vector<uint8_t> buildCreateInstructionData(
    const String &login,
    const String &blockchainName,
    const String &serverAddress,
    const uint8_t rootPub[32],
    const uint8_t devicePub[32],
    const uint8_t blockchainPub[32],
    const uint8_t lastBlockSignature[64],
    const uint8_t rootSignature[64],
    uint64_t createdAtMs) {
  std::vector<uint8_t> out;
  out.reserve(512);
  out.push_back(3);
  pushStrU8(out, login);
  pushFixed(out, rootPub, 32);
  pushU64LE(out, createdAtMs);
  pushU64LE(out, 0);
  pushFixed(out, devicePub, 32);
  pushFixed(out, blockchainPub, 32);
  pushStrU8(out, blockchainName);
  pushU64LE(out, 0);
  pushU32LE(out, 0);
  out.insert(out.end(), 32, 0);
  pushFixed(out, lastBlockSignature, 64);
  out.push_back(0);
  out.push_back(1);
  out.push_back(0);
  pushStrU8(out, serverAddress);
  out.push_back(0);
  out.push_back(0);
  out.push_back(1);
  out.push_back(0);
  out.push_back(0);
  pushFixed(out, rootSignature, 64);
  return out;
}

static std::vector<uint8_t> buildEd25519InstructionData(const uint8_t signature[64], const uint8_t publicKey[32], const uint8_t messageHash[32]) {
  const uint16_t sigOff = 16;
  const uint16_t pkOff = sigOff + 64;
  const uint16_t msgOff = pkOff + 32;
  std::vector<uint8_t> out(msgOff + 32, 0);
  out[0] = 1;
  out[1] = 0;
  out[2] = (uint8_t)(sigOff & 0xFF);
  out[3] = (uint8_t)((sigOff >> 8) & 0xFF);
  out[6] = (uint8_t)(pkOff & 0xFF);
  out[7] = (uint8_t)((pkOff >> 8) & 0xFF);
  out[10] = (uint8_t)(msgOff & 0xFF);
  out[11] = (uint8_t)((msgOff >> 8) & 0xFF);
  out[12] = 32;
  out[4] = out[8] = out[14] = 0xFF;
  out[5] = out[9] = out[15] = 0xFF;
  memcpy(out.data() + sigOff, signature, 64);
  memcpy(out.data() + pkOff, publicKey, 32);
  memcpy(out.data() + msgOff, messageHash, 32);
  return out;
}

static String buildBaseRpcRequest(const char *method, const String &paramsJson) {
  return "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"" + String(method) + "\",\"params\":" + paramsJson + "}";
}

static bool rpcCall(const char *method, const String &paramsJson, String &payloadOut) {
  int code = -1;
  if (!httpPostJson(gData.rpcUrl, buildBaseRpcRequest(method, paramsJson), code, payloadOut)) {
    return false;
  }
  return code >= 200 && code < 300;
}

static bool rpcResponseHasError(const String &payload) {
  return payload.indexOf("\"error\"") >= 0;
}

static bool getLatestBlockhashBytes(uint8_t out[32], String &blockhashB58, String &messageOut) {
  String payload;
  if (!rpcCall("getLatestBlockhash", "[{\"commitment\":\"confirmed\"}]", payload)) {
    messageOut = "RPC не вернул blockhash";
    return false;
  }
  blockhashB58 = jsonStringField(payload, "blockhash");
  if (blockhashB58.length() == 0) {
    messageOut = "В ответе нет blockhash";
    return false;
  }
  std::vector<uint8_t> raw;
  if (!base58ToBytes(blockhashB58, raw) || raw.size() != 32) {
    messageOut = "Некорректный blockhash";
    return false;
  }
  memcpy(out, raw.data(), 32);
  return true;
}

static bool pdaAlreadyExists(const String &login, String &pdaAddress, String &messageOut) {
  uint8_t userPda[32];
  std::vector<std::vector<uint8_t>> seeds = {
    std::vector<uint8_t>((const uint8_t *)kUsersSeedPrefix, (const uint8_t *)kUsersSeedPrefix + strlen(kUsersSeedPrefix)),
    std::vector<uint8_t>((const uint8_t *)login.c_str(), (const uint8_t *)login.c_str() + login.length())
  };
  if (!findProgramAddress(seeds, kShineUsersProgramId, userPda)) {
    messageOut = "Не удалось вычислить user PDA";
    return false;
  }
  pdaAddress = bytesToBase58(userPda, 32);
  String payload;
  if (!rpcCall("getAccountInfo", "[\"" + pdaAddress + "\",{\"encoding\":\"base64\",\"commitment\":\"confirmed\"}]", payload)) {
    messageOut = "Не удалось проверить PDA";
    return false;
  }
  if (payload.indexOf("\"value\":null") >= 0) {
    return false;
  }
  if (payload.indexOf("\"value\"") >= 0) {
    messageOut = "Такой логин уже зарегистрирован";
    return true;
  }
  messageOut = "Непонятный ответ getAccountInfo";
  return false;
}

static std::vector<uint8_t> buildLegacyMessage(
    const uint8_t recentBlockhash[32],
    const uint8_t devicePub[32],
    const uint8_t userPda[32],
    const uint8_t inflowVault[32],
    const uint8_t economyConfig[32],
    const std::vector<uint8_t> &edRootData,
    const std::vector<uint8_t> &edBchData,
    const std::vector<uint8_t> &createData) {
  uint8_t systemProgram[32];
  uint8_t ed25519Program[32];
  uint8_t sysvarInstructions[32];
  uint8_t usersProgram[32];
  uint8_t loginGuardProgram[32];
  base58ToFixed32(kSystemProgramId, systemProgram);
  base58ToFixed32(kEd25519ProgramId, ed25519Program);
  base58ToFixed32(kSysvarInstructionsId, sysvarInstructions);
  base58ToFixed32(kShineUsersProgramId, usersProgram);
  base58ToFixed32(kShineLoginGuardProgramId, loginGuardProgram);

  std::vector<std::vector<uint8_t>> accountKeys;
  accountKeys.emplace_back(devicePub, devicePub + 32);
  accountKeys.emplace_back(userPda, userPda + 32);
  accountKeys.emplace_back(inflowVault, inflowVault + 32);
  accountKeys.emplace_back(systemProgram, systemProgram + 32);
  accountKeys.emplace_back(sysvarInstructions, sysvarInstructions + 32);
  accountKeys.emplace_back(economyConfig, economyConfig + 32);
  accountKeys.emplace_back(loginGuardProgram, loginGuardProgram + 32);
  accountKeys.emplace_back(ed25519Program, ed25519Program + 32);
  accountKeys.emplace_back(usersProgram, usersProgram + 32);

  std::vector<uint8_t> msg;
  msg.reserve(512);
  msg.push_back(1);
  msg.push_back(0);
  msg.push_back(6);
  shortVecEncode(accountKeys.size(), msg);
  for (const auto &key : accountKeys) {
    msg.insert(msg.end(), key.begin(), key.end());
  }
  msg.insert(msg.end(), recentBlockhash, recentBlockhash + 32);
  shortVecEncode(3, msg);

  msg.push_back(7);
  msg.push_back(0);
  shortVecEncode(edRootData.size(), msg);
  msg.insert(msg.end(), edRootData.begin(), edRootData.end());

  msg.push_back(7);
  msg.push_back(0);
  shortVecEncode(edBchData.size(), msg);
  msg.insert(msg.end(), edBchData.begin(), edBchData.end());

  msg.push_back(8);
  msg.push_back(7);
  msg.push_back(0);
  msg.push_back(1);
  msg.push_back(3);
  msg.push_back(2);
  msg.push_back(4);
  msg.push_back(5);
  msg.push_back(6);
  shortVecEncode(createData.size(), msg);
  msg.insert(msg.end(), createData.begin(), createData.end());
  return msg;
}

static bool signMessageEd25519(const std::vector<uint8_t> &message, const uint8_t secretKey[64], uint8_t signature[64]) {
  return crypto_sign_ed25519_detached(signature, nullptr, message.data(), (unsigned long long)message.size(), secretKey) == 0;
}

static String encodeTransactionBase64(const uint8_t signature[64], const std::vector<uint8_t> &message) {
  std::vector<uint8_t> tx;
  tx.reserve(1 + 64 + message.size());
  shortVecEncode(1, tx);
  pushFixed(tx, signature, 64);
  tx.insert(tx.end(), message.begin(), message.end());
  return base64Encode(tx.data(), tx.size());
}

static bool awaitTransactionConfirmation(const String &signatureB58, String &messageOut) {
  for (int attempt = 0; attempt < 15; attempt++) {
    String payload;
    if (!rpcCall("getSignatureStatuses", "[[\"" + signatureB58 + "\"],{\"searchTransactionHistory\":true}]", payload)) {
      delay(1000);
      continue;
    }
    if (payload.indexOf("\"err\":null") >= 0 &&
        (payload.indexOf("\"confirmationStatus\":\"confirmed\"") >= 0 ||
         payload.indexOf("\"confirmationStatus\":\"finalized\"") >= 0)) {
      return true;
    }
    if (payload.indexOf("\"err\":{") >= 0 || payload.indexOf("\"err\":\"") >= 0) {
      messageOut = "Транзакция отклонена сетью";
      return false;
    }
    delay(1000);
  }
  messageOut = "RPC не подтвердил транзакцию вовремя";
  return false;
}

static bool registerHomeserverOnSolana(String &messageOut) {
  messageOut = "";
  if (!gDerivedKeys.ready) {
    if (!restoreDerivedKeysFromSecret()) {
      messageOut = "Секрет повреждён, пересоздайте аккаунт";
      return false;
    }
  }
  if (!isValidLoginValue(gData.login)) {
    messageOut = "Логин должен быть a-z, 0-9, _ и длиной 1..20";
    return false;
  }
  if (WiFi.status() != WL_CONNECTED) {
    messageOut = "Сначала подключите Wi-Fi";
    return false;
  }
  if (gData.wsUrl.length() == 0) {
    messageOut = "Сначала задайте WS URL";
    return false;
  }
  String existingPda;
  String pdaCheckMessage;
  if (pdaAlreadyExists(gData.login, existingPda, pdaCheckMessage)) {
    gData.userPdaAddress = existingPda;
    gData.registered = true;
    gData.online = true;
    saveData();
    messageOut = "PDA уже существует для этого логина";
    return true;
  }
  if (pdaCheckMessage == "Не удалось вычислить user PDA" || pdaCheckMessage == "Не удалось проверить PDA" || pdaCheckMessage == "Непонятный ответ getAccountInfo") {
    messageOut = pdaCheckMessage;
    return false;
  }

  uint8_t userPda[32];
  uint8_t economyConfig[32];
  uint8_t inflowVault[32];
  if (!findProgramAddress({
      std::vector<uint8_t>((const uint8_t *)kUsersSeedPrefix, (const uint8_t *)kUsersSeedPrefix + strlen(kUsersSeedPrefix)),
      std::vector<uint8_t>((const uint8_t *)gData.login.c_str(), (const uint8_t *)gData.login.c_str() + gData.login.length())
    }, kShineUsersProgramId, userPda) ||
      !findProgramAddress({
      std::vector<uint8_t>((const uint8_t *)kUsersEconomyConfigSeed, (const uint8_t *)kUsersEconomyConfigSeed + strlen(kUsersEconomyConfigSeed))
    }, kShineUsersProgramId, economyConfig) ||
      !findProgramAddress({
      std::vector<uint8_t>((const uint8_t *)kPaymentsInflowSeed, (const uint8_t *)kPaymentsInflowSeed + strlen(kPaymentsInflowSeed))
    }, kShinePaymentsProgramId, inflowVault)) {
    messageOut = "Не удалось вычислить обязательные PDA";
    return false;
  }

  String blockchainName = gData.login + "-001";
  std::vector<uint8_t> lastBlockState = buildLastBlockStateBytes(gData.login, blockchainName);
  uint8_t lastBlockHash[32];
  uint8_t lastBlockSignature[64];
  if (!sha256Raw(lastBlockState.data(), lastBlockState.size(), lastBlockHash) ||
      crypto_sign_ed25519_detached(lastBlockSignature, nullptr, lastBlockHash, 32, gDerivedKeys.blockchainSk) != 0) {
    messageOut = "Не удалось подписать LastBlockState";
    return false;
  }

  uint64_t createdAtMs = (uint64_t)millis() + 1704067200000ULL;
  std::vector<uint8_t> unsignedRecord = buildUnsignedCreateRecord(
    gData.login, blockchainName, gData.wsUrl,
    gDerivedKeys.rootPub, gDerivedKeys.devicePub, gDerivedKeys.blockchainPub,
    lastBlockSignature, createdAtMs);
  uint8_t unsignedHash[32];
  uint8_t rootSignature[64];
  if (!sha256Raw(unsignedRecord.data(), unsignedRecord.size(), unsignedHash) ||
      crypto_sign_ed25519_detached(rootSignature, nullptr, unsignedHash, 32, gDerivedKeys.rootSk) != 0) {
    messageOut = "Не удалось подписать PDA-запись";
    return false;
  }

  std::vector<uint8_t> createData = buildCreateInstructionData(
    gData.login, blockchainName, gData.wsUrl,
    gDerivedKeys.rootPub, gDerivedKeys.devicePub, gDerivedKeys.blockchainPub,
    lastBlockSignature, rootSignature, createdAtMs);
  std::vector<uint8_t> edRootData = buildEd25519InstructionData(rootSignature, gDerivedKeys.rootPub, unsignedHash);
  std::vector<uint8_t> edBchData = buildEd25519InstructionData(lastBlockSignature, gDerivedKeys.blockchainPub, lastBlockHash);

  uint8_t recentBlockhash[32];
  String recentBlockhash58;
  if (!getLatestBlockhashBytes(recentBlockhash, recentBlockhash58, messageOut)) {
    return false;
  }

  std::vector<uint8_t> message = buildLegacyMessage(
    recentBlockhash,
    gDerivedKeys.devicePub,
    userPda,
    inflowVault,
    economyConfig,
    edRootData,
    edBchData,
    createData);
  uint8_t txSignature[64];
  if (!signMessageEd25519(message, gDerivedKeys.deviceSk, txSignature)) {
    messageOut = "Не удалось подписать Solana-транзакцию";
    return false;
  }
  String txBase64 = encodeTransactionBase64(txSignature, message);
  String signatureB58 = bytesToBase58(txSignature, 64);

  String payload;
  if (!rpcCall("sendTransaction", "[\"" + txBase64 + "\",{\"encoding\":\"base64\",\"preflightCommitment\":\"confirmed\"}]", payload)) {
    messageOut = "RPC не принял транзакцию";
    return false;
  }
  if (rpcResponseHasError(payload)) {
    messageOut = "RPC вернул ошибку sendTransaction";
    return false;
  }
  if (!awaitTransactionConfirmation(signatureB58, messageOut)) {
    return false;
  }

  gData.userPdaAddress = bytesToBase58(userPda, 32);
  gData.registrationSignature = signatureB58;
  gData.registered = true;
  gData.online = true;
  saveData();
  messageOut = "Solana-регистрация подтверждена";
  return true;
}

static String defaultApiUrl() {
  return "https://shineup.me/api";
}

static String defaultRpcUrl() {
  return "https://api.devnet.solana.com";
}

static String defaultWsUrl() {
  return "wss://shineup.me/ws";
}

static void resetRuntimeNetState() {
  gNetState.wifiStatus = "не проверялся";
  gNetState.localIp = "-";
  gNetState.apiStatus = "не проверялся";
  gNetState.rpcStatus = "не проверялся";
  gNetState.wsStatus = "не проверялся";
  gNetState.serverVersion = "-";
  gNetState.rssi = 0;
}

static void syncWifiRuntimeState() {
  wl_status_t status = WiFi.status();
  if (status == WL_CONNECTED) {
    gNetState.wifiStatus = "подключён";
    gNetState.localIp = WiFi.localIP().toString();
    gNetState.rssi = WiFi.RSSI();
  } else {
    gData.wifiReady = false;
    gData.online = false;
    gNetState.wifiStatus = "отключён";
    gNetState.localIp = "-";
    gNetState.rssi = 0;
  }
}

static bool isHttpUrl(const String &url) {
  return startsWithAny(url, "http://", "https://");
}

static bool isWsUrl(const String &url) {
  return startsWithAny(url, "ws://", "wss://");
}

static bool parseUrlHostPort(const String &url, String &hostOut, uint16_t &portOut, bool &secureOut) {
  secureOut = false;
  portOut = 0;
  int schemeEnd = url.indexOf("://");
  if (schemeEnd <= 0) {
    return false;
  }
  String scheme = url.substring(0, schemeEnd);
  secureOut = (scheme == "https" || scheme == "wss");
  int hostStart = schemeEnd + 3;
  int slash = url.indexOf('/', hostStart);
  String hostPort = slash >= 0 ? url.substring(hostStart, slash) : url.substring(hostStart);
  int atPos = hostPort.lastIndexOf('@');
  if (atPos >= 0) {
    hostPort = hostPort.substring(atPos + 1);
  }
  int colon = hostPort.lastIndexOf(':');
  if (colon > 0 && hostPort.indexOf(']') < 0) {
    hostOut = hostPort.substring(0, colon);
    portOut = (uint16_t)hostPort.substring(colon + 1).toInt();
  } else {
    hostOut = hostPort;
    portOut = secureOut ? 443 : 80;
  }
  hostOut.trim();
  return hostOut.length() > 0 && portOut > 0;
}

static bool httpGetReachable(const String &url, int &statusCode, String &payload) {
  statusCode = -1;
  payload = "";
  if (!isHttpUrl(url)) {
    return false;
  }
  HTTPClient http;
  WiFiClientSecure secureClient;
  WiFiClient plainClient;
  bool secure = url.startsWith("https://");
  if (secure) {
    secureClient.setInsecure();
    if (!http.begin(secureClient, url)) {
      return false;
    }
  } else {
    if (!http.begin(plainClient, url)) {
      return false;
    }
  }
  http.setTimeout(5000);
  statusCode = http.GET();
  if (statusCode > 0) {
    payload = http.getString();
  }
  http.end();
  return statusCode > 0;
}

static bool httpPostJson(const String &url, const String &body, int &statusCode, String &payload) {
  statusCode = -1;
  payload = "";
  if (!isHttpUrl(url)) {
    return false;
  }
  HTTPClient http;
  WiFiClientSecure secureClient;
  WiFiClient plainClient;
  bool secure = url.startsWith("https://");
  if (secure) {
    secureClient.setInsecure();
    if (!http.begin(secureClient, url)) {
      return false;
    }
  } else {
    if (!http.begin(plainClient, url)) {
      return false;
    }
  }
  http.setTimeout(7000);
  http.addHeader("Content-Type", "application/json");
  statusCode = http.POST(body);
  if (statusCode > 0) {
    payload = http.getString();
  }
  http.end();
  return statusCode > 0;
}

static bool tcpProbeWsEndpoint(const String &url) {
  if (!isWsUrl(url)) {
    return false;
  }
  String host;
  uint16_t port = 0;
  bool secure = false;
  if (!parseUrlHostPort(url, host, port, secure)) {
    return false;
  }
  if (secure) {
    WiFiClientSecure client;
    client.setInsecure();
    bool ok = client.connect(host.c_str(), port, 4000);
    client.stop();
    return ok;
  }
  WiFiClient client;
  bool ok = client.connect(host.c_str(), port, 4000);
  client.stop();
  return ok;
}

static String jsonStringField(const String &json, const String &field) {
  String needle = "\"" + field + "\"";
  int keyPos = json.indexOf(needle);
  if (keyPos < 0) {
    return "";
  }
  int colon = json.indexOf(':', keyPos + needle.length());
  if (colon < 0) {
    return "";
  }
  int firstQuote = json.indexOf('"', colon + 1);
  if (firstQuote < 0) {
    return "";
  }
  int endQuote = firstQuote + 1;
  while (true) {
    endQuote = json.indexOf('"', endQuote);
    if (endQuote < 0) {
      return "";
    }
    if (json[endQuote - 1] != '\\') {
      break;
    }
    endQuote++;
  }
  return json.substring(firstQuote + 1, endQuote);
}

static bool jsonInt64Field(const String &json, const String &field, uint64_t &valueOut) {
  String needle = "\"" + field + "\"";
  int keyPos = json.indexOf(needle);
  if (keyPos < 0) {
    return false;
  }
  int colon = json.indexOf(':', keyPos + needle.length());
  if (colon < 0) {
    return false;
  }
  int pos = colon + 1;
  while (pos < (int)json.length() && (json[pos] == ' ' || json[pos] == '\n' || json[pos] == '\r' || json[pos] == '\t')) {
    pos++;
  }
  int start = pos;
  while (pos < (int)json.length() && isDigit((unsigned char)json[pos])) {
    pos++;
  }
  if (pos == start) {
    return false;
  }
  valueOut = strtoull(json.substring(start, pos).c_str(), nullptr, 10);
  return true;
}

static bool connectWifiNow(String &messageOut) {
  messageOut = "";
  gData.wifiReady = false;
  gData.online = false;
  syncWifiRuntimeState();
  if (gData.wifiSsid.length() == 0 || gData.wifiPassword.length() == 0) {
    messageOut = "Нужны SSID и пароль";
    gNetState.wifiStatus = "нет данных";
    return false;
  }
  WiFi.mode(WIFI_STA);
  WiFi.setAutoReconnect(true);
  WiFi.begin(gData.wifiSsid.c_str(), gData.wifiPassword.c_str());
  unsigned long startedAt = millis();
  while (WiFi.status() != WL_CONNECTED && millis() - startedAt < 15000) {
    delay(250);
  }
  syncWifiRuntimeState();
  if (WiFi.status() == WL_CONNECTED) {
    gData.wifiReady = true;
    messageOut = "Wi-Fi подключён";
    return true;
  }
  WiFi.disconnect(true, false);
  syncWifiRuntimeState();
  gNetState.wifiStatus = "ошибка подключения";
  messageOut = "Не удалось подключиться к Wi-Fi";
  return false;
}

static void disconnectWifiNow() {
  WiFi.disconnect(true, false);
  gData.wifiReady = false;
  gData.online = false;
  gData.serversReady = false;
  syncWifiRuntimeState();
  gNetState.apiStatus = "отключён";
  gNetState.rpcStatus = "отключён";
  gNetState.wsStatus = "отключён";
  gNetState.serverVersion = "-";
}

static bool probeApiServer(String &messageOut) {
  messageOut = "";
  int code = -1;
  String payload;
  if (!httpGetReachable(gData.apiUrl, code, payload)) {
    gNetState.apiStatus = "нет ответа";
    messageOut = "API недоступен";
    return false;
  }
  gNetState.apiStatus = "HTTP " + String(code);
  return true;
}

static bool probeRpcServer(String &messageOut) {
  messageOut = "";
  int code = -1;
  String payload;
  String req = "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"getHealth\"}";
  if (!httpPostJson(gData.rpcUrl, req, code, payload)) {
    gNetState.rpcStatus = "нет ответа";
    messageOut = "RPC недоступен";
    return false;
  }
  bool ok = payload.indexOf("\"result\":\"ok\"") >= 0 || payload.indexOf("\"healthy\"") >= 0 || code == 200;
  gNetState.rpcStatus = ok ? "RPC OK" : ("HTTP " + String(code));
  return ok;
}

static bool probeWsServer(String &messageOut) {
  messageOut = "";
  bool ok = tcpProbeWsEndpoint(gData.wsUrl);
  gNetState.wsStatus = ok ? "сокет открыт" : "нет сокета";
  if (!ok) {
    messageOut = "WS недоступен";
  }
  return ok;
}

static bool refreshServerReadiness(String &messageOut) {
  messageOut = "";
  if (WiFi.status() != WL_CONNECTED) {
    gData.serversReady = false;
    messageOut = "Сначала подключите Wi-Fi";
    return false;
  }
  String apiMsg;
  String rpcMsg;
  String wsMsg;
  bool apiOk = probeApiServer(apiMsg);
  bool rpcOk = probeRpcServer(rpcMsg);
  bool wsOk = probeWsServer(wsMsg);
  gData.serversReady = apiOk && rpcOk && wsOk;
  if (gData.serversReady) {
    messageOut = "Серверы доступны";
  } else if (apiMsg.length() > 0) {
    messageOut = apiMsg;
  } else if (rpcMsg.length() > 0) {
    messageOut = rpcMsg;
  } else if (wsMsg.length() > 0) {
    messageOut = wsMsg;
  } else {
    messageOut = "Не все серверы доступны";
  }
  return gData.serversReady;
}

static bool refreshWalletBalance(String &messageOut) {
  messageOut = "";
  if (WiFi.status() != WL_CONNECTED) {
    messageOut = "Сначала подключите Wi-Fi";
    return false;
  }
  if (gData.walletAddress.length() == 0) {
    messageOut = "Сначала создайте кошелёк";
    return false;
  }
  int code = -1;
  String payload;
  String req = "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"getBalance\",\"params\":[\"" + gData.walletAddress + "\",{\"commitment\":\"confirmed\"}]}";
  if (!httpPostJson(gData.rpcUrl, req, code, payload)) {
    gNetState.rpcStatus = "нет ответа";
    messageOut = "Не удалось прочитать баланс";
    return false;
  }
  uint64_t lamports = 0;
  if (!jsonInt64Field(payload, "value", lamports)) {
    gNetState.rpcStatus = "плохой ответ";
    messageOut = "RPC вернул непонятный баланс";
    return false;
  }
  gData.balanceCentiSol = (uint16_t)((lamports * 100ULL) / 1000000000ULL);
  gNetState.rpcStatus = "баланс обновлён";
  messageOut = "Баланс обновлён";
  saveData();
  return true;
}

static void seedRequests() {
  gRequests[0].type = "Вход в сессию";
  gRequests[0].actor = "Chrome / aidarkc";
  gRequests[0].details = "Клиент просит подключиться к homeserverу и открыть сессию без ввода пароля.";
  gRequests[0].status = "Ожидает";

  gRequests[1].type = "Подпись сообщения";
  gRequests[1].actor = "Wallet bridge";
  gRequests[1].details = "Нужно подписать тестовое сообщение для подключения браузерного кошелька.";
  gRequests[1].status = "Ожидает";
}

static void loadDefaults() {
  gData.pin = "1234";
  gData.wifiSsid = "";
  gData.wifiPassword = "";
  gData.login = "";
  gData.homeserverName = "homeserver1";
  gData.secret = "";
  gData.walletAddress = "";
  gData.userPdaAddress = "";
  gData.registrationSignature = "";
  gData.apiUrl = defaultApiUrl();
  gData.rpcUrl = defaultRpcUrl();
  gData.wsUrl = defaultWsUrl();
  gData.balanceCentiSol = 0;
  gData.wifiReady = false;
  gData.serversReady = false;
  gData.secretReady = false;
  gData.registered = false;
  gData.online = false;
  resetRuntimeNetState();
}

static void saveData() {
  gPrefs.putString("pin", gData.pin);
  gPrefs.putString("wifi_ssid", gData.wifiSsid);
  gPrefs.putString("wifi_pass", gData.wifiPassword);
  gPrefs.putString("login", gData.login);
  gPrefs.putString("homeserver", gData.homeserverName);
  gPrefs.putString("secret", gData.secret);
  gPrefs.putString("wallet", gData.walletAddress);
  gPrefs.putString("user_pda", gData.userPdaAddress);
  gPrefs.putString("reg_sig", gData.registrationSignature);
  gPrefs.putString("api", gData.apiUrl);
  gPrefs.putString("rpc", gData.rpcUrl);
  gPrefs.putString("ws", gData.wsUrl);
  gPrefs.putUShort("bal", gData.balanceCentiSol);
  gPrefs.putBool("wifi_ok", gData.wifiReady);
  gPrefs.putBool("srv_ok", gData.serversReady);
  gPrefs.putBool("secret_ok", gData.secretReady);
  gPrefs.putBool("registered", gData.registered);
  gPrefs.putBool("online", gData.online);
}

static void loadData() {
  loadDefaults();
  gData.pin = gPrefs.getString("pin", gData.pin);
  gData.wifiSsid = gPrefs.getString("wifi_ssid", gData.wifiSsid);
  gData.wifiPassword = gPrefs.getString("wifi_pass", gData.wifiPassword);
  gData.login = gPrefs.getString("login", gData.login);
  gData.homeserverName = gPrefs.getString("homeserver", gData.homeserverName);
  gData.secret = gPrefs.getString("secret", gData.secret);
  gData.walletAddress = gPrefs.getString("wallet", gData.walletAddress);
  gData.userPdaAddress = gPrefs.getString("user_pda", gData.userPdaAddress);
  gData.registrationSignature = gPrefs.getString("reg_sig", gData.registrationSignature);
  gData.apiUrl = gPrefs.getString("api", gData.apiUrl);
  gData.rpcUrl = gPrefs.getString("rpc", gData.rpcUrl);
  gData.wsUrl = gPrefs.getString("ws", gData.wsUrl);
  gData.balanceCentiSol = gPrefs.getUShort("bal", gData.balanceCentiSol);
  gData.wifiReady = gPrefs.getBool("wifi_ok", gData.wifiReady);
  gData.serversReady = gPrefs.getBool("srv_ok", gData.serversReady);
  gData.secretReady = gPrefs.getBool("secret_ok", gData.secretReady);
  gData.registered = gPrefs.getBool("registered", gData.registered);
  gData.online = gPrefs.getBool("online", gData.online);
  restoreDerivedKeysFromSecret();
  if (gData.registered && gData.userPdaAddress.length() == 0) {
    gData.registered = false;
    gData.online = false;
  }
}

static void fullResetData() {
  gPrefs.clear();
  loadDefaults();
  gLockBuffer = "";
  gEditBuffer = "";
  gEditTarget = EDIT_NONE;
  gConfirmTarget = CONFIRM_NONE;
  gNotice = "Устройство сброшено";
  seedRequests();
  saveData();
}

static void generateSecretAndWallet() {
  if (!deriveFreshSecretAndWallet()) {
    gData.secret = "";
    gData.walletAddress = "";
    gData.secretReady = false;
    return;
  }
  gData.userPdaAddress = "";
  gData.registrationSignature = "";
  gData.registered = false;
  gData.online = false;
  if (gData.homeserverName.length() == 0) {
    gData.homeserverName = "homeserver1";
  }
  saveData();
}

static ActionId digitAction(int digit) {
  return (ActionId)(ACT_DIGIT_0 + digit);
}

static void drawNumericPad(int yStart, bool withCancel = false) {
  int index = 1;
  int x0 = 44;
  int y = yStart;
  for (int row = 0; row < 3; row++) {
    for (int col = 0; col < 3; col++) {
      int x = x0 + col * 128;
      addButton(x, y, 92, 54, digitAction(index), String(index), true, C_BUTTON);
      index++;
    }
    y += 60;
  }
  addButton(44, y, 92, 54, withCancel ? ACT_INPUT_CANCEL : ACT_INPUT_BACKSPACE, withCancel ? "Отмена" : "Стереть", true, C_BUTTON2);
  addButton(172, y, 92, 54, ACT_DIGIT_0, "0", true, C_BUTTON);
  addButton(300, y, 92, 54, ACT_INPUT_OK, "OK", true, C_OK);
}

static void drawKeyboard() {
  const char **rows = gKeyboardAlt ? KB_ALT : KB_ALPHA;
  int keyW = 40;
  int keyH = 42;
  int gap = 4;
  int y = 232;
  for (int row = 0; row < 3; row++) {
    String letters = rows[row];
    int totalW = letters.length() * (keyW + gap) - gap;
    int x = (DISP_W - totalW) / 2;
    for (int i = 0; i < letters.length(); i++) {
      String key = letters.substring(i, i + 1);
      ActionId act = (ActionId)(1000 + key[0]);
      addButton(x, y, keyW, keyH, act, key, true, C_BUTTON);
      x += keyW + gap;
    }
    y += keyH + gap;
  }
  addButton(20, 370, 84, 44, ACT_KB_SHIFT, gKeyboardAlt ? "abc" : "123", true, C_BUTTON2);
  addButton(112, 370, 124, 44, ACT_KB_SPACE, "Пробел", true, C_BUTTON2);
  addButton(244, 370, 92, 44, ACT_INPUT_BACKSPACE, "Стереть", true, C_BUTTON2);
  addButton(344, 370, 116, 44, ACT_INPUT_OK, "Сохранить", true, C_OK);
  addButton(20, 420, 440, 40, ACT_INPUT_CANCEL, "Отмена", true, C_CARD);
}

static String editTargetLabel() {
  switch (gEditTarget) {
    case EDIT_SSID: return "SSID";
    case EDIT_WIFI_PASSWORD: return "Пароль Wi-Fi";
    case EDIT_LOGIN: return "Логин";
    case EDIT_HOMESERVER: return "Имя homeserver";
    case EDIT_API: return "API URL";
    case EDIT_RPC: return "RPC URL";
    case EDIT_WS: return "WS URL";
    case EDIT_NEW_PIN: return "Новый PIN";
    case EDIT_UNLOCK_PIN: return "PIN";
    default: return "Поле";
  }
}

static void drawLockScreen() {
  clearButtons();
  drawTopBar("SHiNE Device");
  drawPanel(20, 96, 440, 110, C_PANEL, C_BORDER, 14);
  drawText(36, 126, "Устройство заблокировано", C_TEXT, (const uint8_t *)FONT_BODY);
  String masked;
  for (int i = 0; i < gLockBuffer.length(); i++) {
    masked += "*";
  }
  drawPanel(36, 142, 408, 46, C_CARD, C_BORDER, 10);
  drawText(52, 172, masked.length() ? masked : "Введите PIN", masked.length() ? C_TEXT : C_MUTE, (const uint8_t *)FONT_BODY);
  drawNoticeBox();
  drawNumericPad(220, true);
}

static void drawHomeScreen() {
  clearButtons();
  drawTopBar("Главный экран");
  drawPanel(20, 92, 440, 98, C_PANEL, C_BORDER, 16);
  drawText(36, 122, registrationSummary(), canRegister() || gData.registered ? C_ACCENT : C_WARN, (const uint8_t *)FONT_HEAD);
  drawText(36, 152, "Логин: " + (gData.login.length() ? gData.login : "не задан"), C_TEXT, (const uint8_t *)FONT_BODY);
  drawText(36, 174, "Homeserver: " + gData.homeserverName, C_MUTE, (const uint8_t *)FONT_BODY);

  drawPanel(20, 204, 210, 82, C_CARD, C_BORDER, 12);
  drawText(34, 232, "Wi-Fi", C_TEXT, (const uint8_t *)FONT_BODY);
  drawText(34, 258, gData.wifiReady ? gData.wifiSsid : "Не настроен", gData.wifiReady ? C_ACCENT : C_WARN, (const uint8_t *)FONT_SMALL);

  drawPanel(250, 204, 210, 82, C_CARD, C_BORDER, 12);
  drawText(264, 232, "Кошелёк", C_TEXT, (const uint8_t *)FONT_BODY);
  drawText(264, 258, balanceText(), gData.balanceCentiSol >= 20 ? C_ACCENT : C_WARN, (const uint8_t *)FONT_SMALL);

  addButton(20, 300, 136, 52, ACT_STATUS, "Статус");
  addButton(172, 300, 136, 52, ACT_CONNECTION, "Подключение");
  addButton(324, 300, 136, 52, ACT_ACCOUNT, "Аккаунт");
  addButton(20, 362, 136, 52, ACT_WALLET, "Кошелёк");
  addButton(172, 362, 136, 52, ACT_REQUESTS, "Запросы");
  addButton(324, 362, 136, 52, ACT_SETTINGS, "Настройки");
  addButton(20, 422, 440, 38, ACT_REGISTER, gData.registered ? "Устройство уже зарегистрировано" : "Зарегистрировать", canRegister(), canRegister() ? C_OK : C_CARD);
  drawNoticeBox();
}

static void drawStatusScreen() {
  clearButtons();
  drawTopBar("Статус");
  drawPanel(20, 92, 440, 286, C_PANEL, C_BORDER, 16);
  drawText(36, 122, "Логин: " + (gData.login.length() ? gData.login : "не задан"), C_TEXT);
  drawText(36, 148, "Homeserver: " + gData.homeserverName, C_TEXT);
  drawText(36, 174, "Секрет: " + boolText(gData.secretReady, "сохранён", "не задан"), gData.secretReady ? C_ACCENT : C_WARN);
  drawText(36, 200, "Отпечаток: " + (gData.secretReady ? shortenValue(gData.secret) : "-"), C_MUTE, (const uint8_t *)FONT_SMALL);
  drawText(36, 226, "Wi-Fi: " + boolText(gData.wifiReady, "готов", "не готов"), gData.wifiReady ? C_ACCENT : C_WARN);
  drawText(36, 252, "Серверы: " + boolText(gData.serversReady, "готовы", "не готовы"), gData.serversReady ? C_ACCENT : C_WARN);
  drawText(36, 278, "Баланс: " + balanceText(), gData.balanceCentiSol >= 20 ? C_ACCENT : C_WARN);
  drawText(36, 304, "Регистрация: " + boolText(gData.registered, "выполнена", "не выполнена"), gData.registered ? C_ACCENT : C_WARN);
  drawText(36, 330, "Онлайн: " + boolText(gData.online), gData.online ? C_ACCENT : C_WARN);
  drawText(36, 356, "PDA/TX: " + registrationDetailsShort(), C_MUTE, (const uint8_t *)FONT_SMALL);
  addButton(20, 396, 212, 48, ACT_BACK, "Назад");
  addButton(248, 396, 212, 48, ACT_REFRESH, "Обновить статус", true, C_BUTTON2);
  drawNoticeBox();
}

static void drawConnectionScreen() {
  clearButtons();
  drawTopBar("Подключение");
  drawPanel(20, 92, 440, 176, C_PANEL, C_BORDER, 16);
  drawText(36, 124, "Wi-Fi: " + boolText(gData.wifiReady, "готов", "не готов"), gData.wifiReady ? C_ACCENT : C_WARN);
  drawText(36, 154, "Серверы: " + boolText(gData.serversReady, "готовы", "не готовы"), gData.serversReady ? C_ACCENT : C_WARN);
  drawText(36, 184, "Онлайн: " + boolText(gData.online, "подключён", "отключён"), gData.online ? C_ACCENT : C_WARN);
  drawText(36, 214, "IP: " + gNetState.localIp, C_MUTE, (const uint8_t *)FONT_SMALL);
  drawText(36, 236, "API: " + gNetState.apiStatus, C_MUTE, (const uint8_t *)FONT_SMALL);
  drawText(36, 258, "RPC: " + gNetState.rpcStatus, C_MUTE, (const uint8_t *)FONT_SMALL);
  drawText(36, 280, "WS: " + gNetState.wsStatus, C_MUTE, (const uint8_t *)FONT_SMALL);
  addButton(20, 286, 212, 48, ACT_WIFI, "Wi-Fi");
  addButton(248, 286, 212, 48, ACT_SERVERS, "Серверы");
  addButton(20, 346, 212, 48, ACT_CONNECT, "Подключить", gData.wifiReady && gData.serversReady, C_OK);
  addButton(248, 346, 212, 48, ACT_DISCONNECT, "Отключить", gData.online, C_BUTTON2);
  addButton(20, 412, 440, 40, ACT_BACK, "Назад");
  drawNoticeBox();
}

static void drawWifiScreen() {
  clearButtons();
  drawTopBar("Wi-Fi");
  drawPanel(20, 92, 440, 174, C_PANEL, C_BORDER, 16);
  drawText(36, 122, "SSID: " + (gData.wifiSsid.length() ? gData.wifiSsid : "не задан"), C_TEXT);
  drawText(36, 152, String("Пароль: ") + (gData.wifiPassword.length() ? "сохранён" : "не задан"), C_TEXT);
  drawText(36, 182, "Статус: " + gNetState.wifiStatus, gData.wifiReady ? C_ACCENT : C_WARN);
  drawText(36, 208, "IP: " + gNetState.localIp, C_MUTE, (const uint8_t *)FONT_SMALL);
  drawText(36, 228, "RSSI: " + String(gNetState.rssi) + " dBm", C_MUTE, (const uint8_t *)FONT_SMALL);
  drawWrappedText(36, 248, 45, 16, "Проверка делает реальное подключение по сохранённым SSID и паролю.", C_MUTE, (const uint8_t *)FONT_SMALL);
  addButton(20, 286, 212, 48, ACT_EDIT_SSID, "Изменить SSID");
  addButton(248, 286, 212, 48, ACT_EDIT_WIFI_PASSWORD, "Изменить пароль");
  addButton(20, 346, 212, 48, ACT_VERIFY_WIFI, "Проверить", true, C_OK);
  addButton(248, 346, 212, 48, ACT_RESET_WIFI, "Сбросить", true, C_BUTTON2);
  addButton(20, 412, 440, 40, ACT_BACK, "Назад");
  drawNoticeBox();
}

static void drawServersScreen() {
  clearButtons();
  drawTopBar("Серверы");
  drawPanel(20, 92, 440, 202, C_PANEL, C_BORDER, 16);
  drawText(36, 120, "API: " + shortenValue(gData.apiUrl, 24, 10), C_TEXT, (const uint8_t *)FONT_SMALL);
  drawText(36, 150, "RPC: " + shortenValue(gData.rpcUrl, 24, 10), C_TEXT, (const uint8_t *)FONT_SMALL);
  drawText(36, 180, "WS: " + shortenValue(gData.wsUrl, 24, 10), C_TEXT, (const uint8_t *)FONT_SMALL);
  drawText(36, 212, "Статус: " + boolText(gData.serversReady, "готовы", "не готовы"), gData.serversReady ? C_ACCENT : C_WARN);
  drawText(36, 238, "API: " + gNetState.apiStatus, C_MUTE, (const uint8_t *)FONT_SMALL);
  drawText(36, 258, "RPC: " + gNetState.rpcStatus, C_MUTE, (const uint8_t *)FONT_SMALL);
  drawText(36, 278, "WS: " + gNetState.wsStatus, C_MUTE, (const uint8_t *)FONT_SMALL);
  addButton(20, 310, 136, 46, ACT_EDIT_API, "Изменить API");
  addButton(172, 310, 136, 46, ACT_EDIT_RPC, "Изменить RPC");
  addButton(324, 310, 136, 46, ACT_EDIT_WS, "Изменить WS");
  addButton(20, 368, 212, 46, ACT_VERIFY_SERVERS, "Проверить", true, C_OK);
  addButton(248, 368, 212, 46, ACT_SET_TEST_SERVERS, "Тестовые", true, C_BUTTON2);
  addButton(20, 420, 440, 36, ACT_BACK, "Назад");
  drawNoticeBox();
}

static void drawAccountScreen() {
  clearButtons();
  drawTopBar("Аккаунт");
  drawPanel(20, 92, 440, 188, C_PANEL, C_BORDER, 16);
  drawText(36, 122, "Логин: " + (gData.login.length() ? gData.login : "не задан"), C_TEXT);
  drawText(36, 152, "Homeserver: " + gData.homeserverName, C_TEXT);
  drawText(36, 182, "Секрет: " + boolText(gData.secretReady, "сохранён", "не задан"), gData.secretReady ? C_ACCENT : C_WARN);
  drawText(36, 212, "Кошелёк: " + (gData.walletAddress.length() ? shortenValue(gData.walletAddress, 10, 8) : "не создан"), C_MUTE, (const uint8_t *)FONT_SMALL);
  drawText(36, 236, "Регистрация: " + boolText(gData.registered, "выполнена", "не выполнена"), gData.registered ? C_ACCENT : C_WARN);
  drawText(36, 260, "PDA: " + registrationDetailsShort(), C_MUTE, (const uint8_t *)FONT_SMALL);
  addButton(20, 300, 212, 48, ACT_EDIT_LOGIN, "Изменить логин");
  addButton(248, 300, 212, 48, ACT_EDIT_HOMESERVER, "Имя homeserver");
  addButton(20, 360, 212, 48, ACT_GENERATE_SECRET, "Сгенерировать", true, C_OK);
  addButton(248, 360, 212, 48, ACT_CLEAR_ACCOUNT, "Очистить", true, C_BUTTON2);
  addButton(20, 420, 440, 36, ACT_BACK, "Назад");
  drawNoticeBox();
}

static void drawWalletScreen() {
  clearButtons();
  drawTopBar("Кошелёк");
  drawPanel(20, 92, 440, 182, C_PANEL, C_BORDER, 16);
  drawText(36, 122, "Адрес: " + (gData.walletAddress.length() ? shortenValue(gData.walletAddress, 14, 10) : "не создан"), C_TEXT, (const uint8_t *)FONT_SMALL);
  drawText(36, 154, "Баланс: " + balanceText(), gData.balanceCentiSol >= 20 ? C_ACCENT : C_WARN);
  drawText(36, 184, "Минимум для регистрации: 0.20 SOL", C_TEXT, (const uint8_t *)FONT_SMALL);
  drawText(36, 214, "Статус: " + String(gData.balanceCentiSol >= 20 ? "Хватает" : "Не хватает"), gData.balanceCentiSol >= 20 ? C_ACCENT : C_WARN);
  drawWrappedText(36, 242, 48, 16, "Проверить читает реальный баланс из Solana RPC. Кнопки +/- оставлены как локальный тестовый режим.", C_MUTE, (const uint8_t *)FONT_SMALL);
  addButton(20, 292, 212, 46, ACT_SHOW_QR, "QR и URI", gData.walletAddress.length() > 0, C_OK);
  addButton(248, 292, 212, 46, ACT_CHECK_BALANCE, "Проверить");
  addButton(20, 350, 136, 46, ACT_BALANCE_PLUS_010, "+0.10 SOL");
  addButton(172, 350, 136, 46, ACT_BALANCE_PLUS_025, "+0.25 SOL", true, C_OK);
  addButton(324, 350, 136, 46, ACT_BALANCE_MINUS_010, "-0.10 SOL", gData.balanceCentiSol > 0, C_BUTTON2);
  addButton(20, 420, 440, 36, ACT_BACK, "Назад");
  drawNoticeBox();
}

static String walletUri() {
  if (gData.walletAddress.length() == 0) {
    return "";
  }
  return "solana:" + gData.walletAddress + "?amount=0.20&label=SHiNE%20Register";
}

static void drawQrCodeBlock(int x, int y, int size, const String &payload) {
  uint8_t qr[qrcodegen_BUFFER_LEN_MAX];
  uint8_t tmp[qrcodegen_BUFFER_LEN_MAX];
  memset(qr, 0, sizeof(qr));
  memset(tmp, 0, sizeof(tmp));
  bool ok = qrcodegen_encodeText(payload.c_str(), tmp, qr, qrcodegen_Ecc_MEDIUM, 1, 10, qrcodegen_Mask_AUTO, true);
  drawPanel(x, y, size, size, 0xFFFFu, 0xFFFFu, 8);
  if (!ok) {
    drawText(x + 18, y + size / 2, "QR ERROR", C_BAD, (const uint8_t *)FONT_BODY);
    return;
  }
  int qrSize = qrcodegen_getSize(qr);
  int scale = (size - 20) / qrSize;
  if (scale < 1) {
    scale = 1;
  }
  int margin = (size - qrSize * scale) / 2;
  gfx->fillRect(x + 4, y + 4, size - 8, size - 8, 0xFFFFu);
  for (int yy = 0; yy < qrSize; yy++) {
    for (int xx = 0; xx < qrSize; xx++) {
      if (qrcodegen_getModule(qr, xx, yy)) {
        gfx->fillRect(x + margin + xx * scale, y + margin + yy * scale, scale, scale, 0x0000u);
      }
    }
  }
}

static void drawWalletQrScreen() {
  clearButtons();
  drawTopBar("QR и URI");
  String uri = walletUri();
  drawQrCodeBlock(36, 96, 240, uri);
  drawPanel(292, 96, 152, 240, C_PANEL, C_BORDER, 14);
  drawWrappedText(306, 126, 14, 18, "Пополнение\nдля\nрегистрации", C_TEXT, (const uint8_t *)FONT_BODY);
  drawText(306, 226, "Сумма", C_MUTE, (const uint8_t *)FONT_SMALL);
  drawText(306, 248, "0.20 SOL", C_ACCENT, (const uint8_t *)FONT_BODY);
  drawText(306, 286, "Баланс", C_MUTE, (const uint8_t *)FONT_SMALL);
  drawText(306, 308, balanceText(), C_TEXT, (const uint8_t *)FONT_BODY);
  drawPanel(20, 350, 440, 92, C_PANEL, C_BORDER, 14);
  drawWrappedText(34, 378, 52, 16, uri, C_TEXT, (const uint8_t *)FONT_SMALL);
  addButton(20, 446, 440, 28, ACT_BACK, "Назад");
}

static void drawRequestsScreen() {
  clearButtons();
  drawTopBar("Запросы");
  for (int i = 0; i < 2; i++) {
    int y = 96 + i * 112;
    drawPanel(20, y, 440, 94, C_PANEL, C_BORDER, 14);
    drawText(34, y + 28, gRequests[i].type, C_TEXT, (const uint8_t *)FONT_BODY);
    drawText(34, y + 54, gRequests[i].actor, C_MUTE, (const uint8_t *)FONT_SMALL);
    drawText(34, y + 74, "Статус: " + gRequests[i].status, gRequests[i].status == "Ожидает" ? C_WARN : C_ACCENT, (const uint8_t *)FONT_SMALL);
    addButton(330, y + 26, 110, 42, i == 0 ? ACT_OPEN_REQ_0 : ACT_OPEN_REQ_1, "Открыть");
  }
  addButton(20, 420, 440, 36, ACT_BACK, "Назад");
  drawNoticeBox();
}

static void drawRequestDetailScreen() {
  clearButtons();
  RequestItem &req = gRequests[gCurrentRequest];
  drawTopBar("Детали запроса");
  drawPanel(20, 92, 440, 248, C_PANEL, C_BORDER, 16);
  drawText(36, 120, req.type, C_TEXT, (const uint8_t *)FONT_HEAD);
  drawText(36, 152, "Источник: " + req.actor, C_TEXT, (const uint8_t *)FONT_BODY);
  drawText(36, 180, "Статус: " + req.status, req.status == "Ожидает" ? C_WARN : C_ACCENT, (const uint8_t *)FONT_BODY);
  drawWrappedText(36, 214, 48, 18, req.details, C_MUTE, (const uint8_t *)FONT_BODY);
  addButton(20, 360, 136, 48, ACT_APPROVE_REQUEST, "Разрешить", req.status == "Ожидает", C_OK);
  addButton(172, 360, 136, 48, ACT_REJECT_REQUEST, "Отклонить", req.status == "Ожидает", C_BUTTON2);
  addButton(324, 360, 136, 48, ACT_BACK, "Назад");
  drawNoticeBox();
}

static void drawSettingsScreen() {
  clearButtons();
  drawTopBar("Настройки");
  drawPanel(20, 92, 440, 176, C_PANEL, C_BORDER, 16);
  drawText(36, 122, "PIN: " + repeatChar('*', gData.pin.length()), C_TEXT);
  drawText(36, 152, String("Вход по PIN: ") + (kPinLockEnabled ? "активен" : "временно отключён"), kPinLockEnabled ? C_TEXT : C_WARN);
  drawText(36, 182, "Русский UI: активен", C_ACCENT);
  drawWrappedText(36, 214, 48, 16, "Русский текст рисуется через UTF-8 и кириллический U8g2-шрифт. Это обязательная часть прототипа.", C_MUTE, (const uint8_t *)FONT_SMALL);
  addButton(20, 286, 212, 48, ACT_CHANGE_PIN, "Сменить PIN");
  addButton(248, 286, 212, 48, ACT_RESET_ONLINE, "Сбросить онлайн", true, C_BUTTON2);
  addButton(20, 346, 440, 48, ACT_FULL_RESET, "Полный сброс", true, C_BAD);
  addButton(20, 420, 440, 36, ACT_BACK, "Назад");
  drawNoticeBox();
}

static void drawTextInputScreen() {
  clearButtons();
  drawTopBar("Редактирование");
  drawPanel(20, 92, 440, 126, C_PANEL, C_BORDER, 16);
  drawText(36, 122, editTargetLabel(), C_TEXT, (const uint8_t *)FONT_HEAD);
  drawPanel(36, 142, 408, 50, C_CARD, C_BORDER, 10);
  String shown = gEditBuffer.length() ? gEditBuffer : "Введите значение";
  drawWrappedText(50, 172, 34, 16, shown, gEditBuffer.length() ? C_TEXT : C_MUTE, (const uint8_t *)FONT_SMALL);
  drawKeyboard();
}

static void drawPinEditScreen() {
  clearButtons();
  drawTopBar("Новый PIN");
  drawPanel(20, 92, 440, 112, C_PANEL, C_BORDER, 16);
  drawText(36, 122, "Введите новый PIN", C_TEXT, (const uint8_t *)FONT_HEAD);
  String masked;
  for (int i = 0; i < gEditBuffer.length(); i++) {
    masked += "*";
  }
  drawPanel(36, 146, 408, 42, C_CARD, C_BORDER, 10);
  drawText(52, 174, masked.length() ? masked : "4-8 цифр", masked.length() ? C_TEXT : C_MUTE, (const uint8_t *)FONT_BODY);
  drawNoticeBox();
  drawNumericPad(220, true);
}

static void drawConfirmScreen() {
  clearButtons();
  drawTopBar("Подтверждение");
  drawPanel(40, 140, 400, 180, C_PANEL, C_BORDER, 18);
  String title = "Подтвердить";
  String text = "Выполнить действие?";
  if (gConfirmTarget == CONFIRM_REGISTER) {
    title = "Регистрация";
    text = "Отправить create_user_pda в Solana через device key этого устройства?";
  } else if (gConfirmTarget == CONFIRM_CLEAR_ACCOUNT) {
    title = "Очистка";
    text = "Удалить секрет, кошелёк и статус регистрации?";
  } else if (gConfirmTarget == CONFIRM_FULL_RESET) {
    title = "Полный сброс";
    text = "Очистить весь локальный конфиг устройства?";
  }
  drawText(64, 180, title, C_TEXT, (const uint8_t *)FONT_HEAD);
  drawWrappedText(64, 218, 34, 18, text, C_MUTE, (const uint8_t *)FONT_BODY);
  addButton(64, 270, 144, 42, ACT_CONFIRM_YES, "Подтвердить", true, C_OK);
  addButton(232, 270, 144, 42, ACT_CONFIRM_NO, "Отмена", true, C_BUTTON2);
}

static void redrawScreen() {
  gNeedRedraw = false;
  if (gScreen == SCR_LOCK) {
    drawLockScreen();
    return;
  }
  switch (gScreen) {
    case SCR_HOME: drawHomeScreen(); break;
    case SCR_STATUS: drawStatusScreen(); break;
    case SCR_CONNECTION: drawConnectionScreen(); break;
    case SCR_WIFI_EDIT: drawWifiScreen(); break;
    case SCR_SERVERS: drawServersScreen(); break;
    case SCR_ACCOUNT: drawAccountScreen(); break;
    case SCR_WALLET: drawWalletScreen(); break;
    case SCR_WALLET_QR: drawWalletQrScreen(); break;
    case SCR_REQUESTS: drawRequestsScreen(); break;
    case SCR_REQUEST_DETAIL: drawRequestDetailScreen(); break;
    case SCR_SETTINGS: drawSettingsScreen(); break;
    case SCR_PIN_EDIT: drawPinEditScreen(); break;
    case SCR_TEXT_INPUT: drawTextInputScreen(); break;
    case SCR_CONFIRM: drawConfirmScreen(); break;
    default: drawHomeScreen(); break;
  }
}

static void openScreen(ScreenId screen) {
  if (gScreen != SCR_LOCK && screen != SCR_CONFIRM && screen != SCR_TEXT_INPUT && screen != SCR_PIN_EDIT) {
    gPrevScreen = gScreen;
  }
  gScreen = screen;
  gNeedRedraw = true;
}

static void openEdit(EditTarget target, const String &startValue, bool alt = false) {
  gEditTarget = target;
  gEditBuffer = startValue;
  gKeyboardAlt = alt;
  openScreen(target == EDIT_NEW_PIN ? SCR_PIN_EDIT : SCR_TEXT_INPUT);
}

static void openConfirm(ConfirmTarget target) {
  gConfirmTarget = target;
  openScreen(SCR_CONFIRM);
}

static void applyEditValue() {
  String value = trimCopy(gEditBuffer);
  switch (gEditTarget) {
    case EDIT_SSID:
      gData.wifiSsid = value;
      gData.wifiReady = false;
      gData.online = false;
      gNetState.wifiStatus = "нужно проверить";
      gNetState.localIp = "-";
      gNotice = "SSID сохранён";
      break;
    case EDIT_WIFI_PASSWORD:
      gData.wifiPassword = gEditBuffer;
      gData.wifiReady = false;
      gData.online = false;
      gNetState.wifiStatus = "нужно проверить";
      gNetState.localIp = "-";
      gNotice = "Пароль Wi-Fi сохранён";
      break;
    case EDIT_LOGIN:
      gData.login = normalizeLogin(value);
      gData.registered = false;
      gData.online = false;
      gData.userPdaAddress = "";
      gData.registrationSignature = "";
      gNotice = "Логин сохранён";
      break;
    case EDIT_HOMESERVER:
      gData.homeserverName = value.length() ? value : "homeserver1";
      gNotice = "Имя homeserver сохранено";
      break;
    case EDIT_API:
      gData.apiUrl = value;
      gData.serversReady = false;
      gData.online = false;
      gNetState.apiStatus = "нужно проверить";
      gNotice = "API URL сохранён";
      break;
    case EDIT_RPC:
      gData.rpcUrl = value;
      gData.serversReady = false;
      gData.online = false;
      gNetState.rpcStatus = "нужно проверить";
      gNotice = "RPC URL сохранён";
      break;
    case EDIT_WS:
      gData.wsUrl = value;
      gData.serversReady = false;
      gData.online = false;
      gNetState.wsStatus = "нужно проверить";
      gNotice = "WS URL сохранён";
      break;
    case EDIT_NEW_PIN:
      if (gEditBuffer.length() < 4 || gEditBuffer.length() > 8) {
        gNotice = "PIN должен быть длиной 4-8";
        return;
      }
      gData.pin = gEditBuffer;
      gNotice = "PIN обновлён";
      break;
    default:
      break;
  }
  saveData();
  gEditTarget = EDIT_NONE;
  gEditBuffer = "";
  openScreen(gPrevScreen);
}

static bool handleCustomKeyAction(ActionId action) {
  if ((int)action >= 1000) {
    char c = (char)((int)action - 1000);
    gEditBuffer += c;
    gNeedRedraw = true;
    return true;
  }
  return false;
}

static void handleAction(ActionId action) {
  if (handleCustomKeyAction(action)) {
    return;
  }

  if (gScreen == SCR_LOCK) {
    if (action >= ACT_DIGIT_0 && action <= ACT_DIGIT_9) {
      if (gLockBuffer.length() < 8) {
        gLockBuffer += String((int)action - (int)ACT_DIGIT_0);
      }
      gNotice = "";
      gNeedRedraw = true;
      return;
    }
    if (action == ACT_INPUT_CANCEL) {
      gLockBuffer = "";
      gNotice = "";
      gNeedRedraw = true;
      return;
    }
    if (action == ACT_INPUT_BACKSPACE) {
      if (gLockBuffer.length() > 0) {
        gLockBuffer.remove(gLockBuffer.length() - 1);
      }
      gNeedRedraw = true;
      return;
    }
    if (action == ACT_INPUT_OK) {
      if (gLockBuffer == gData.pin) {
        gLockBuffer = "";
        gNotice = "";
        openScreen(SCR_HOME);
      } else {
        gLockBuffer = "";
        gNotice = "Неверный PIN";
        gNeedRedraw = true;
      }
      return;
    }
  }

  if (gScreen == SCR_TEXT_INPUT) {
    if (action == ACT_KB_SHIFT) {
      gKeyboardAlt = !gKeyboardAlt;
      gNeedRedraw = true;
      return;
    }
    if (action == ACT_KB_SPACE) {
      gEditBuffer += " ";
      gNeedRedraw = true;
      return;
    }
    if (action == ACT_INPUT_BACKSPACE) {
      if (gEditBuffer.length() > 0) {
        gEditBuffer.remove(gEditBuffer.length() - 1);
      }
      gNeedRedraw = true;
      return;
    }
    if (action == ACT_INPUT_CANCEL) {
      gEditTarget = EDIT_NONE;
      gEditBuffer = "";
      openScreen(gPrevScreen);
      return;
    }
    if (action == ACT_INPUT_OK) {
      applyEditValue();
      return;
    }
  }

  if (gScreen == SCR_PIN_EDIT) {
    if (action >= ACT_DIGIT_0 && action <= ACT_DIGIT_9) {
      if (gEditBuffer.length() < 8) {
        gEditBuffer += String((int)action - (int)ACT_DIGIT_0);
      }
      gNeedRedraw = true;
      return;
    }
    if (action == ACT_INPUT_BACKSPACE) {
      if (gEditBuffer.length() > 0) {
        gEditBuffer.remove(gEditBuffer.length() - 1);
      }
      gNeedRedraw = true;
      return;
    }
    if (action == ACT_INPUT_CANCEL) {
      gEditTarget = EDIT_NONE;
      gEditBuffer = "";
      openScreen(gPrevScreen);
      return;
    }
    if (action == ACT_INPUT_OK) {
      applyEditValue();
      return;
    }
  }

  if (gScreen == SCR_CONFIRM) {
    if (action == ACT_CONFIRM_NO) {
      gConfirmTarget = CONFIRM_NONE;
      openScreen(gPrevScreen);
      return;
    }
    if (action == ACT_CONFIRM_YES) {
      if (gConfirmTarget == CONFIRM_REGISTER) {
        registerHomeserverOnSolana(gNotice);
      } else if (gConfirmTarget == CONFIRM_CLEAR_ACCOUNT) {
        gData.secret = "";
        gData.walletAddress = "";
        gData.userPdaAddress = "";
        gData.registrationSignature = "";
        gData.secretReady = false;
        gData.registered = false;
        gData.online = false;
        gNotice = "Секрет и регистрация очищены";
        saveData();
      } else if (gConfirmTarget == CONFIRM_FULL_RESET) {
        fullResetData();
      }
      gConfirmTarget = CONFIRM_NONE;
      openScreen(SCR_HOME);
      return;
    }
  }

  switch (action) {
    case ACT_HOME: openScreen(SCR_HOME); break;
    case ACT_BACK: openScreen(gPrevScreen == gScreen ? SCR_HOME : gPrevScreen); break;
    case ACT_STATUS: openScreen(SCR_STATUS); break;
    case ACT_CONNECTION: openScreen(SCR_CONNECTION); break;
    case ACT_ACCOUNT: openScreen(SCR_ACCOUNT); break;
    case ACT_WALLET: openScreen(SCR_WALLET); break;
    case ACT_REQUESTS: openScreen(SCR_REQUESTS); break;
    case ACT_SETTINGS: openScreen(SCR_SETTINGS); break;
    case ACT_REFRESH:
      syncWifiRuntimeState();
      gNotice = "Статус обновлён";
      gNeedRedraw = true;
      break;
    case ACT_WIFI: openScreen(SCR_WIFI_EDIT); break;
    case ACT_SERVERS: openScreen(SCR_SERVERS); break;
    case ACT_CONNECT:
      if (WiFi.status() == WL_CONNECTED && gData.serversReady) {
        gData.online = true;
        gNotice = "Устройство переведено в онлайн";
        saveData();
      } else {
        gNotice = "Сначала проверьте Wi-Fi и серверы";
      }
      gNeedRedraw = true;
      break;
    case ACT_DISCONNECT:
      gData.online = false;
      gNotice = "Онлайн отключён";
      saveData();
      gNeedRedraw = true;
      break;
    case ACT_EDIT_SSID: openEdit(EDIT_SSID, gData.wifiSsid, false); break;
    case ACT_EDIT_WIFI_PASSWORD: openEdit(EDIT_WIFI_PASSWORD, gData.wifiPassword, true); break;
    case ACT_VERIFY_WIFI:
      connectWifiNow(gNotice);
      saveData();
      gNeedRedraw = true;
      break;
    case ACT_RESET_WIFI:
      gData.wifiSsid = "";
      gData.wifiPassword = "";
      disconnectWifiNow();
      gNotice = "Wi-Fi очищен";
      saveData();
      gNeedRedraw = true;
      break;
    case ACT_EDIT_API: openEdit(EDIT_API, gData.apiUrl, true); break;
    case ACT_EDIT_RPC: openEdit(EDIT_RPC, gData.rpcUrl, true); break;
    case ACT_EDIT_WS: openEdit(EDIT_WS, gData.wsUrl, true); break;
    case ACT_VERIFY_SERVERS:
      if (gData.apiUrl.length() == 0 || gData.rpcUrl.length() == 0 || gData.wsUrl.length() == 0) {
        gData.serversReady = false;
        gNotice = "Заполните все адреса";
      } else {
        refreshServerReadiness(gNotice);
      }
      saveData();
      gNeedRedraw = true;
      break;
    case ACT_SET_TEST_SERVERS:
      gData.apiUrl = defaultApiUrl();
      gData.rpcUrl = defaultRpcUrl();
      gData.wsUrl = defaultWsUrl();
      gData.serversReady = false;
      gData.online = false;
      gNetState.apiStatus = "нужно проверить";
      gNetState.rpcStatus = "нужно проверить";
      gNetState.wsStatus = "нужно проверить";
      gNotice = "Подставлены тестовые серверы";
      saveData();
      gNeedRedraw = true;
      break;
    case ACT_EDIT_LOGIN: openEdit(EDIT_LOGIN, gData.login, false); break;
    case ACT_EDIT_HOMESERVER: openEdit(EDIT_HOMESERVER, gData.homeserverName, false); break;
    case ACT_GENERATE_SECRET:
      generateSecretAndWallet();
      gNotice = gData.secretReady ? "Секрет сгенерирован, device-кошелёк выведен из него" : "Не удалось сгенерировать секрет";
      gNeedRedraw = true;
      break;
    case ACT_CLEAR_ACCOUNT:
      openConfirm(CONFIRM_CLEAR_ACCOUNT);
      break;
    case ACT_SHOW_QR:
      if (gData.walletAddress.length() > 0) {
        openScreen(SCR_WALLET_QR);
      }
      break;
    case ACT_BALANCE_PLUS_010:
      gData.balanceCentiSol += 10;
      gNotice = "Баланс увеличен";
      saveData();
      gNeedRedraw = true;
      break;
    case ACT_BALANCE_PLUS_025:
      gData.balanceCentiSol += 25;
      gNotice = "Баланс увеличен";
      saveData();
      gNeedRedraw = true;
      break;
    case ACT_BALANCE_MINUS_010:
      if (gData.balanceCentiSol >= 10) {
        gData.balanceCentiSol -= 10;
      } else {
        gData.balanceCentiSol = 0;
      }
      gNotice = "Баланс уменьшен";
      saveData();
      gNeedRedraw = true;
      break;
    case ACT_CHECK_BALANCE:
      if (!refreshWalletBalance(gNotice)) {
        if (gData.balanceCentiSol >= 20) {
          gNotice += " Средств хватает.";
        }
      }
      gNeedRedraw = true;
      break;
    case ACT_REGISTER:
      if (canRegister()) {
        openConfirm(CONFIRM_REGISTER);
      } else {
        gNotice = "Пока не выполнены все условия регистрации";
        gNeedRedraw = true;
      }
      break;
    case ACT_OPEN_REQ_0:
      gCurrentRequest = 0;
      openScreen(SCR_REQUEST_DETAIL);
      break;
    case ACT_OPEN_REQ_1:
      gCurrentRequest = 1;
      openScreen(SCR_REQUEST_DETAIL);
      break;
    case ACT_APPROVE_REQUEST:
      gRequests[gCurrentRequest].status = "Разрешён";
      gNotice = "Запрос разрешён";
      gNeedRedraw = true;
      break;
    case ACT_REJECT_REQUEST:
      gRequests[gCurrentRequest].status = "Отклонён";
      gNotice = "Запрос отклонён";
      gNeedRedraw = true;
      break;
    case ACT_CHANGE_PIN:
      openEdit(EDIT_NEW_PIN, "", false);
      break;
    case ACT_RESET_ONLINE:
      gData.online = false;
      gNotice = "Онлайн сброшен";
      saveData();
      gNeedRedraw = true;
      break;
    case ACT_FULL_RESET:
      openConfirm(CONFIRM_FULL_RESET);
      break;
    default:
      break;
  }
}

static ActionId actionAt(int16_t x, int16_t y) {
  for (int i = gButtonCount - 1; i >= 0; i--) {
    Button &btn = gButtons[i];
    if (!btn.enabled) {
      continue;
    }
    if (x >= btn.x && x < btn.x + btn.w && y >= btn.y && y < btn.y + btn.h) {
      return btn.action;
    }
  }
  return ACT_NONE;
}

static void pollTouch() {
  int16_t x = 0;
  int16_t y = 0;
  bool touching = gTouch.getPoint(&x, &y, 1) > 0;
  if (touching && !gTouchDown) {
    gTouchDown = true;
    gTouchStartX = x;
    gTouchStartY = y;
    gTouchLastX = x;
    gTouchLastY = y;
    return;
  }
  if (touching && gTouchDown) {
    gTouchLastX = x;
    gTouchLastY = y;
    return;
  }
  if (!touching && gTouchDown) {
    gTouchDown = false;
    int dx = abs(gTouchLastX - gTouchStartX);
    int dy = abs(gTouchLastY - gTouchStartY);
    if (dx < 14 && dy < 14) {
      ActionId action = actionAt(gTouchLastX, gTouchLastY);
      if (action != ACT_NONE) {
        handleAction(action);
      }
    }
  }
}

void setup() {
  Serial.begin(115200);
  sodium_init();
  Wire.begin(PIN_I2C_SDA, PIN_I2C_SCL);

  gfx->begin();
  gBus->writeC8D8(0x36, 0xA0);
  gfx->setBrightness(220);
  gfx->fillScreen(C_BG);
  gfx->setUTF8Print(true);

  gTouch.setPins(PIN_TP_INT, -1);
  gTouch.begin(Wire, CST92XX_SLAVE_ADDRESS, PIN_I2C_SDA, PIN_I2C_SCL);
  gTouch.setMaxCoordinates(DISP_W, DISP_H);
  gTouch.setSwapXY(true);
  gTouch.setMirrorXY(true, false);

  gPrefs.begin("shine-ui", false);
  loadData();
  if (!kPinLockEnabled) {
    gScreen = SCR_HOME;
    gPrevScreen = SCR_HOME;
    gLockBuffer = "";
  }
  syncWifiRuntimeState();
  if (WiFi.status() != WL_CONNECTED) {
    gData.online = false;
    if (gData.wifiReady) {
      gNetState.wifiStatus = "нужно проверить";
      gNetState.localIp = "-";
    }
  }
  seedRequests();
  gNotice = "Русский UTF-8 UI активен";
  gNeedRedraw = true;
}

void loop() {
  syncWifiRuntimeState();
  pollTouch();
  if (gNeedRedraw) {
    redrawScreen();
  }
  delay(16);
}
