#include <inttypes.h>
#include <ctype.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>

#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "mbedtls/sha256.h"
#include "cJSON.h"
#include "secp256k1.h"
#include "secp256k1_extrakeys.h"
#include "secp256k1_schnorrsig.h"

#include "bech32.h"
#include "buttons.h"
#include "display.h"
#include "key_derivation.h"
#include "mnemonic.h"
#include "usb_transport.h"
#include "ui.h"
#include "secure_mem.h"
#include "nip004.h"
#include "nip044.h"
#include "nostr_common.h"

#define FIRMWARE_VERSION "0.0.1"
#ifndef GIT_HASH
#define GIT_HASH "dev"
#endif

static const char *TAG = "nsigner";

/* Debug aid: skip startup menu and auto-generate a fresh mnemonic each boot. */
#define CYD_DEBUG_AUTO_GENERATE 0

#define APPROVAL_TIMEOUT_MS 30000

typedef enum {
    APPROVAL_DENY = 0,
    APPROVAL_ONCE = 1,
    APPROVAL_ALWAYS = 2,
    APPROVAL_TIMEOUT = 3,
} approval_decision_t;

static bool s_always_allow_sign = false;

#define AUTH_REQUIRED 1
#define AUTH_KIND 27235
#define AUTH_NONCE_CACHE 32
static uint8_t s_nonce_cache[AUTH_NONCE_CACHE][32];
static int s_nonce_count = 0;
static int s_nonce_next = 0;

#define AUTH_ERR_ENVELOPE_MALFORMED 2010
#define AUTH_ERR_BODY_MISMATCH 2011
#define AUTH_ERR_SIGNATURE_INVALID 2012
#define AUTH_ERR_KIND_INVALID 2013
#define AUTH_ERR_ENVELOPE_REQUIRED 2014
#define AUTH_ERR_REPLAY_DETECTED 2017

/* Keep large buffers off the main task stack (ESP32-S3 default stack is limited). */
static char s_mnemonic[256];
static uint8_t s_seed[64];
static uint8_t s_privkey[32];
static uint8_t s_pubkey[32];
static char s_npub[128];
static uint8_t s_frame_buf[4096];
static char s_pubkey_hex[65];
static char s_response_buf[2048];
static char s_signed_event_buf[1600];

static char s_encrypt_buf[1600];

static int transport_init(void)
{
    return usb_transport_init();
}

static approval_decision_t wait_for_approval(uint32_t timeout_ms)
{
    ui_approval_decision_t ui_decision = ui_wait_for_approval(timeout_ms);

    if (ui_decision == UI_APPROVAL_DENY) {
        return APPROVAL_DENY;
    }
    if (ui_decision == UI_APPROVAL_ONCE) {
        return APPROVAL_ONCE;
    }
    if (ui_decision == UI_APPROVAL_ALWAYS) {
        return APPROVAL_ALWAYS;
    }
    return APPROVAL_TIMEOUT;
}

static void show_approval_prompt_with_caller(const cJSON *event_in, const char *caller_pubkey_hex)
{
    int kind = 1;
    const char *content = "";
    const cJSON *kind_item = cJSON_GetObjectItemCaseSensitive((cJSON *)event_in, "kind");
    const cJSON *content_item = cJSON_GetObjectItemCaseSensitive((cJSON *)event_in, "content");

    if (cJSON_IsNumber(kind_item)) {
        kind = kind_item->valueint;
    }
    if (cJSON_IsString(content_item) && content_item->valuestring != NULL) {
        content = content_item->valuestring;
    }

    ui_show_approval_prompt(caller_pubkey_hex, kind, content, APPROVAL_TIMEOUT_MS);
}

static approval_decision_t prompt_non_sign_approval(const char *caller_pubkey_hex, int kind, const char *content)
{
    if (s_always_allow_sign) {
        return APPROVAL_ALWAYS;
    }

    ui_show_approval_prompt(caller_pubkey_hex, kind, content ? content : "", APPROVAL_TIMEOUT_MS);
    approval_decision_t decision = wait_for_approval(APPROVAL_TIMEOUT_MS);
    if (decision == APPROVAL_ALWAYS) {
        s_always_allow_sign = true;
    }
    return decision;
}

static void show_idle_screen(void)
{
    char version_line[24];
    snprintf(version_line, sizeof(version_line), "v%s", FIRMWARE_VERSION);
    ui_show_idle(s_npub, s_mnemonic, version_line);
}

static int recv_frame_stdin(uint8_t *payload, size_t payload_max, size_t *out_len)
{
    return cdc_recv_frame(payload, payload_max, out_len);
}

static int send_frame_stdout(const uint8_t *payload, size_t len)
{
    return cdc_send_frame(payload, len);
}

static void bytes_to_hex(const uint8_t *in, size_t in_len, char *out, size_t out_len)
{
    static const char hex[] = "0123456789abcdef";
    size_t i;

    if (in == NULL || out == NULL || out_len < (in_len * 2U + 1U)) {
        return;
    }

    for (i = 0; i < in_len; ++i) {
        out[i * 2U] = hex[(in[i] >> 4) & 0x0F];
        out[i * 2U + 1U] = hex[in[i] & 0x0F];
    }
    out[in_len * 2U] = '\0';
}

static int extract_json_id_token(const char *json, char *out, size_t out_len)
{
    const char *id_pos;
    const char *p;
    size_t n = 0;

    if (json == NULL || out == NULL || out_len < 5) {
        return -1;
    }

    id_pos = strstr(json, "\"id\"");
    if (id_pos == NULL) {
        return -1;
    }

    p = strchr(id_pos, ':');
    if (p == NULL) {
        return -1;
    }
    ++p;
    while (*p != '\0' && isspace((unsigned char)*p)) {
        ++p;
    }

    if (*p == '\"') {
        out[n++] = '\"';
        ++p;
        while (*p != '\0' && *p != '\"' && n + 2 < out_len) {
            out[n++] = *p++;
        }
        if (*p != '\"') {
            return -1;
        }
        out[n++] = '\"';
        out[n] = '\0';
        return 0;
    }

    while (*p != '\0' && *p != ',' && *p != '}' && !isspace((unsigned char)*p) && n + 1 < out_len) {
        out[n++] = *p++;
    }

    if (n == 0) {
        return -1;
    }

    out[n] = '\0';
    return 0;
}

static int extract_json_string_field(const char *json, const char *field, char *out, size_t out_len)
{
    char key[48];
    const char *pos;
    const char *p;
    size_t n = 0;

    if (json == NULL || field == NULL || out == NULL || out_len < 2) {
        return -1;
    }

    snprintf(key, sizeof(key), "\"%s\"", field);
    pos = strstr(json, key);
    if (pos == NULL) {
        return -1;
    }

    p = strchr(pos, ':');
    if (p == NULL) {
        return -1;
    }
    ++p;

    while (*p != '\0' && isspace((unsigned char)*p)) {
        ++p;
    }
    if (*p != '\"') {
        return -1;
    }

    ++p;
    while (*p != '\0' && *p != '\"' && n + 1 < out_len) {
        out[n++] = *p++;
    }

    if (*p != '\"' || n == 0) {
        return -1;
    }

    out[n] = '\0';
    return 0;
}

static int sha256_bytes(const uint8_t *in, size_t in_len, uint8_t out32[32])
{
    if (in == NULL || out32 == NULL) {
        return -1;
    }

    mbedtls_sha256(in, in_len, out32, 0);

    return 0;
}

static int build_signed_event_json(const cJSON *event_in,
                                   const char *pubkey_hex,
                                   const uint8_t privkey[32],
                                   char *out,
                                   size_t out_len)
{
    const cJSON *kind_item;
    const cJSON *created_item;
    const cJSON *content_item;
    const cJSON *tags_item;
    int kind = 1;
    int64_t created_at = 0;
    const char *content = "";
    cJSON *ser_arr = NULL;
    cJSON *result_obj = NULL;
    cJSON *tags_dup = NULL;
    char *ser_json = NULL;
    char *result_json = NULL;
    uint8_t id32[32] = {0};
    uint8_t sig64[64] = {0};
    char id_hex[65] = {0};
    char sig_hex[129] = {0};
    int rc = -1;

    if (event_in == NULL || pubkey_hex == NULL || privkey == NULL || out == NULL || out_len == 0) {
        return -1;
    }

    kind_item = cJSON_GetObjectItemCaseSensitive((cJSON *)event_in, "kind");
    created_item = cJSON_GetObjectItemCaseSensitive((cJSON *)event_in, "created_at");
    content_item = cJSON_GetObjectItemCaseSensitive((cJSON *)event_in, "content");
    tags_item = cJSON_GetObjectItemCaseSensitive((cJSON *)event_in, "tags");

    if (cJSON_IsNumber(kind_item)) {
        kind = kind_item->valueint;
    }
    if (cJSON_IsNumber(created_item)) {
        created_at = (int64_t)created_item->valuedouble;
    }
    if (cJSON_IsString(content_item) && content_item->valuestring != NULL) {
        content = content_item->valuestring;
    }

    ser_arr = cJSON_CreateArray();
    if (ser_arr == NULL) {
        goto cleanup;
    }

    cJSON_AddItemToArray(ser_arr, cJSON_CreateNumber(0));
    cJSON_AddItemToArray(ser_arr, cJSON_CreateString(pubkey_hex));
    cJSON_AddItemToArray(ser_arr, cJSON_CreateNumber((double)created_at));
    cJSON_AddItemToArray(ser_arr, cJSON_CreateNumber(kind));

    if (cJSON_IsArray(tags_item)) {
        tags_dup = cJSON_Duplicate((cJSON *)tags_item, 1);
    } else {
        tags_dup = cJSON_CreateArray();
    }
    if (tags_dup == NULL) {
        goto cleanup;
    }
    cJSON_AddItemToArray(ser_arr, tags_dup);
    tags_dup = NULL;

    cJSON_AddItemToArray(ser_arr, cJSON_CreateString(content));

    ser_json = cJSON_PrintUnformatted(ser_arr);
    if (ser_json == NULL) {
        goto cleanup;
    }

    if (sha256_bytes((const uint8_t *)ser_json, strlen(ser_json), id32) != 0) {
        goto cleanup;
    }
    bytes_to_hex(id32, sizeof(id32), id_hex, sizeof(id_hex));

    if (schnorr_sign32(privkey, id32, sig64) != 0) {
        goto cleanup;
    }
    bytes_to_hex(sig64, sizeof(sig64), sig_hex, sizeof(sig_hex));

    result_obj = cJSON_CreateObject();
    if (result_obj == NULL) {
        goto cleanup;
    }

    cJSON_AddStringToObject(result_obj, "id", id_hex);
    cJSON_AddStringToObject(result_obj, "pubkey", pubkey_hex);
    cJSON_AddNumberToObject(result_obj, "created_at", (double)created_at);
    cJSON_AddNumberToObject(result_obj, "kind", kind);
    if (cJSON_IsArray(tags_item)) {
        cJSON_AddItemToObject(result_obj, "tags", cJSON_Duplicate((cJSON *)tags_item, 1));
    } else {
        cJSON_AddItemToObject(result_obj, "tags", cJSON_CreateArray());
    }
    cJSON_AddStringToObject(result_obj, "content", content);
    cJSON_AddStringToObject(result_obj, "sig", sig_hex);

    result_json = cJSON_PrintUnformatted(result_obj);
    if (result_json == NULL) {
        goto cleanup;
    }

    if (strlen(result_json) + 1 > out_len) {
        goto cleanup;
    }

    memcpy(out, result_json, strlen(result_json) + 1);
    rc = 0;

cleanup:
    if (ser_arr != NULL) {
        cJSON_Delete(ser_arr);
    }
    if (result_obj != NULL) {
        cJSON_Delete(result_obj);
    }
    if (tags_dup != NULL) {
        cJSON_Delete(tags_dup);
    }
    if (ser_json != NULL) {
        cJSON_free(ser_json);
    }
    if (result_json != NULL) {
        cJSON_free(result_json);
    }

    memset(id32, 0, sizeof(id32));
    memset(sig64, 0, sizeof(sig64));
    return rc;
}

static int hex_to_bytes(const char *hex, uint8_t *out, size_t out_len)
{
    size_t hex_len;
    size_t i;

    if (hex == NULL || out == NULL) {
        return -1;
    }

    hex_len = strlen(hex);
    if (hex_len != out_len * 2U) {
        return -1;
    }

    for (i = 0; i < out_len; ++i) {
        char hi = hex[i * 2U];
        char lo = hex[i * 2U + 1U];
        int hv, lv;

        if (hi >= '0' && hi <= '9') hv = hi - '0';
        else if (hi >= 'a' && hi <= 'f') hv = 10 + (hi - 'a');
        else if (hi >= 'A' && hi <= 'F') hv = 10 + (hi - 'A');
        else return -1;

        if (lo >= '0' && lo <= '9') lv = lo - '0';
        else if (lo >= 'a' && lo <= 'f') lv = 10 + (lo - 'a');
        else if (lo >= 'A' && lo <= 'F') lv = 10 + (lo - 'A');
        else return -1;

        out[i] = (uint8_t)((hv << 4) | lv);
    }
    return 0;
}

static int auth_replay_check_and_record(const uint8_t id32[32])
{
    int i;
    int slot;

    for (i = 0; i < s_nonce_count; ++i) {
        if (memcmp(s_nonce_cache[i], id32, 32) == 0) {
            return -1;
        }
    }

    slot = s_nonce_next;
    memcpy(s_nonce_cache[slot], id32, 32);
    s_nonce_next = (s_nonce_next + 1) % AUTH_NONCE_CACHE;
    if (s_nonce_count < AUTH_NONCE_CACHE) {
        s_nonce_count++;
    }
    return 0;
}

/*
 * Verify the auth envelope for an inbound JSON-RPC request.
 *
 * Returns:
 *   0  -> envelope valid, caller_pubkey_hex_out filled in (65 bytes incl. NUL)
 *   >0 -> error code (matches AUTH_ERR_* constants)
 *
 * PoC-grade: no wall-clock skew check (no RTC). Replay protection via
 * in-memory nonce cache of recent auth event ids.
 */
static int auth_envelope_verify(cJSON *req,
                                const char *method,
                                char caller_pubkey_hex_out[65])
{
    cJSON *auth_obj = cJSON_GetObjectItemCaseSensitive(req, "auth");
    cJSON *params = cJSON_GetObjectItemCaseSensitive(req, "params");
    cJSON *id_item;
    cJSON *pk_item;
    cJSON *created_item;
    cJSON *kind_item;
    cJSON *tags_item;
    cJSON *content_item;
    cJSON *sig_item;
    cJSON *ser_arr = NULL;
    char *ser_json = NULL;
    char *params_json = NULL;
    char body_hash_hex[65];
    uint8_t computed_id[32];
    uint8_t expected_id[32];
    uint8_t pubkey32[32];
    uint8_t sig64[64];
    secp256k1_xonly_pubkey xonly;
    secp256k1_context *ctx = NULL;
    int rc = AUTH_ERR_ENVELOPE_MALFORMED;
    int found_method = 0;
    int found_body = 0;
    cJSON *tag_entry;
    int kind_value;

    if (auth_obj == NULL || !cJSON_IsObject(auth_obj)) {
        return AUTH_ERR_ENVELOPE_REQUIRED;
    }

    id_item = cJSON_GetObjectItemCaseSensitive(auth_obj, "id");
    pk_item = cJSON_GetObjectItemCaseSensitive(auth_obj, "pubkey");
    created_item = cJSON_GetObjectItemCaseSensitive(auth_obj, "created_at");
    kind_item = cJSON_GetObjectItemCaseSensitive(auth_obj, "kind");
    tags_item = cJSON_GetObjectItemCaseSensitive(auth_obj, "tags");
    content_item = cJSON_GetObjectItemCaseSensitive(auth_obj, "content");
    sig_item = cJSON_GetObjectItemCaseSensitive(auth_obj, "sig");

    if (!cJSON_IsString(id_item) || !cJSON_IsString(pk_item) ||
        !cJSON_IsNumber(created_item) || !cJSON_IsNumber(kind_item) ||
        !cJSON_IsArray(tags_item) || !cJSON_IsString(content_item) ||
        !cJSON_IsString(sig_item)) {
        return AUTH_ERR_ENVELOPE_MALFORMED;
    }

    kind_value = kind_item->valueint;
    if (kind_value != AUTH_KIND) {
        return AUTH_ERR_KIND_INVALID;
    }

    if (hex_to_bytes(id_item->valuestring, expected_id, 32) != 0 ||
        hex_to_bytes(pk_item->valuestring, pubkey32, 32) != 0 ||
        hex_to_bytes(sig_item->valuestring, sig64, 64) != 0) {
        return AUTH_ERR_ENVELOPE_MALFORMED;
    }

    ser_arr = cJSON_CreateArray();
    if (ser_arr == NULL) {
        return AUTH_ERR_ENVELOPE_MALFORMED;
    }
    cJSON_AddItemToArray(ser_arr, cJSON_CreateNumber(0));
    cJSON_AddItemToArray(ser_arr, cJSON_CreateString(pk_item->valuestring));
    cJSON_AddItemToArray(ser_arr, cJSON_CreateNumber(created_item->valuedouble));
    cJSON_AddItemToArray(ser_arr, cJSON_CreateNumber(kind_value));
    cJSON_AddItemToArray(ser_arr, cJSON_Duplicate(tags_item, 1));
    cJSON_AddItemToArray(ser_arr, cJSON_CreateString(content_item->valuestring));

    ser_json = cJSON_PrintUnformatted(ser_arr);
    if (ser_json == NULL) {
        rc = AUTH_ERR_ENVELOPE_MALFORMED;
        goto done;
    }

    mbedtls_sha256((const uint8_t *)ser_json, strlen(ser_json), computed_id, 0);

    if (memcmp(computed_id, expected_id, 32) != 0) {
        rc = AUTH_ERR_ENVELOPE_MALFORMED;
        goto done;
    }

    ctx = secp256k1_context_create(SECP256K1_CONTEXT_VERIFY);
    if (ctx == NULL) {
        rc = AUTH_ERR_SIGNATURE_INVALID;
        goto done;
    }
    if (!secp256k1_xonly_pubkey_parse(ctx, &xonly, pubkey32)) {
        rc = AUTH_ERR_SIGNATURE_INVALID;
        goto done;
    }
    if (!secp256k1_schnorrsig_verify(ctx, sig64, expected_id, 32, &xonly)) {
        rc = AUTH_ERR_SIGNATURE_INVALID;
        goto done;
    }

    {
        cJSON *params_for_hash = params != NULL ? params : cJSON_CreateArray();
        params_json = cJSON_PrintUnformatted(params_for_hash);
    }
    if (params_json == NULL) {
        rc = AUTH_ERR_BODY_MISMATCH;
        goto done;
    }
    {
        uint8_t body_hash[32];
        size_t i;
        static const char hex[] = "0123456789abcdef";

        mbedtls_sha256((const uint8_t *)params_json, strlen(params_json), body_hash, 0);
        for (i = 0; i < 32; ++i) {
            body_hash_hex[i * 2U] = hex[(body_hash[i] >> 4) & 0x0F];
            body_hash_hex[i * 2U + 1U] = hex[body_hash[i] & 0x0F];
        }
        body_hash_hex[64] = '\0';
    }

    cJSON_ArrayForEach(tag_entry, tags_item) {
        cJSON *k_item;
        cJSON *v_item;

        if (!cJSON_IsArray(tag_entry) || cJSON_GetArraySize(tag_entry) < 2) {
            continue;
        }
        k_item = cJSON_GetArrayItem(tag_entry, 0);
        v_item = cJSON_GetArrayItem(tag_entry, 1);
        if (!cJSON_IsString(k_item) || !cJSON_IsString(v_item)) {
            continue;
        }
        if (strcmp(k_item->valuestring, "nsigner_method") == 0 &&
            strcmp(v_item->valuestring, method) == 0) {
            found_method = 1;
        } else if (strcmp(k_item->valuestring, "nsigner_body_hash") == 0 &&
                   strcasecmp(v_item->valuestring, body_hash_hex) == 0) {
            found_body = 1;
        }
    }

    if (!found_method || !found_body) {
        rc = AUTH_ERR_BODY_MISMATCH;
        goto done;
    }

    if (auth_replay_check_and_record(expected_id) != 0) {
        rc = AUTH_ERR_REPLAY_DETECTED;
        goto done;
    }

    memcpy(caller_pubkey_hex_out, pk_item->valuestring, 64);
    caller_pubkey_hex_out[64] = '\0';
    rc = 0;

done:
    if (ser_arr != NULL) {
        cJSON_Delete(ser_arr);
    }
    if (ser_json != NULL) {
        cJSON_free(ser_json);
    }
    if (params_json != NULL) {
        cJSON_free(params_json);
    }
    if (ctx != NULL) {
        secp256k1_context_destroy(ctx);
    }
    return rc;
}

static int parse_nostr_index_from_params(cJSON *params, uint32_t *out_index)
{
    int params_count;
    cJSON *last_item;
    cJSON *idx_item;
    int idx;

    if (out_index == NULL) {
        return -1;
    }

    *out_index = 0;

    if (params == NULL || !cJSON_IsArray(params)) {
        return 0;
    }

    params_count = cJSON_GetArraySize(params);
    if (params_count <= 0) {
        return 0;
    }

    last_item = cJSON_GetArrayItem(params, params_count - 1);
    if (last_item == NULL || !cJSON_IsObject(last_item)) {
        return 0;
    }

    idx_item = cJSON_GetObjectItemCaseSensitive(last_item, "nostr_index");
    if (idx_item == NULL) {
        return 0;
    }
    if (!cJSON_IsNumber(idx_item)) {
        return -1;
    }

    idx = idx_item->valueint;
    if (idx < 0) {
        return -1;
    }

    *out_index = (uint32_t)idx;
    return 0;
}

static int derive_request_key(uint32_t nostr_index, uint8_t privkey_out[32], char pubkey_hex_out[65])
{
    uint8_t pubkey[32];

    if (privkey_out == NULL || pubkey_hex_out == NULL) {
        return -1;
    }

    if (derive_nostr_key_index(s_seed, nostr_index, privkey_out, pubkey) != 0) {
        return -1;
    }

    bytes_to_hex(pubkey, sizeof(pubkey), pubkey_hex_out, 65);
    secure_memzero(pubkey, sizeof(pubkey));
    return 0;
}

static int parse_peer_and_message_params(cJSON *params,
                                         const char **peer_hex_out,
                                         const char **message_out,
                                         uint32_t *nostr_index_out)
{
    cJSON *peer_item;
    cJSON *msg_item;

    if (params == NULL || !cJSON_IsArray(params) ||
        peer_hex_out == NULL || message_out == NULL || nostr_index_out == NULL) {
        return -1;
    }

    if (cJSON_GetArraySize(params) < 2) {
        return -1;
    }

    peer_item = cJSON_GetArrayItem(params, 0);
    msg_item = cJSON_GetArrayItem(params, 1);

    if (!cJSON_IsString(peer_item) || peer_item->valuestring == NULL ||
        !cJSON_IsString(msg_item) || msg_item->valuestring == NULL) {
        return -1;
    }

    if (parse_nostr_index_from_params(params, nostr_index_out) != 0) {
        return -1;
    }

    *peer_hex_out = peer_item->valuestring;
    *message_out = msg_item->valuestring;
    return 0;
}

static int apply_mnemonic_and_enter_working(const char *mnemonic)
{
    if (mnemonic == NULL || mnemonic[0] == '\0') {
        return -1;
    }

    if (mnemonic_to_seed(mnemonic, s_seed) != 0) {
        ESP_ERROR_CHECK(display_draw_text(10, 50, "seed failed", RGB565_RED, RGB565_BLACK));
        return -1;
    }

    if (derive_nostr_key_index(s_seed, 0u, s_privkey, s_pubkey) != 0) {
        ESP_ERROR_CHECK(display_draw_text(10, 50, "key failed", RGB565_RED, RGB565_BLACK));
        secure_memzero(s_seed, sizeof(s_seed));
        return -1;
    }

    if (npub_encode(s_pubkey, s_npub, sizeof(s_npub)) != 0) {
        ESP_ERROR_CHECK(display_draw_text(10, 50, "npub failed", RGB565_RED, RGB565_BLACK));
        secure_memzero(s_seed, sizeof(s_seed));
        secure_memzero(s_pubkey, sizeof(s_pubkey));
        return -1;
    }

    bytes_to_hex(s_pubkey, sizeof(s_pubkey), s_pubkey_hex, sizeof(s_pubkey_hex));
    printf("Derived npub[0]: %s\n", s_npub);
    printf("Derived pubkey hex[0]: %s\n", s_pubkey_hex);

    show_idle_screen();

    secure_memzero(s_pubkey, sizeof(s_pubkey));
    return 0;
}

static void run_signing_loop(void)
{
    uint32_t rx_frames = 0;
    uint32_t tx_frames = 0;
    size_t last_len = 0;

    while (1) {
        char stat1[32];
        char stat2[32];
        int got_frame = 0;
        int via_webusb = 0;

        if (webusb_recv_frame(s_frame_buf, sizeof(s_frame_buf), &last_len) == 0) {
            got_frame = 1;
            via_webusb = 1;
        } else if (recv_frame_stdin(s_frame_buf, sizeof(s_frame_buf), &last_len) == 0) {
            got_frame = 1;
            via_webusb = 0;
        }

        if (!got_frame) {
            ui_pump();
            vTaskDelay(pdMS_TO_TICKS(2));
            continue;
        }

        rx_frames++;

        /* Default to a concrete error so every received frame gets a response. */
        snprintf(
            s_response_buf,
            sizeof(s_response_buf),
            "{\"jsonrpc\":\"2.0\",\"id\":null,\"error\":{\"code\":-32603,\"message\":\"internal error: unhandled request\"}}");

        if (last_len < sizeof(s_frame_buf)) {
            char id_token[96] = {0};
            char method[64] = {0};
            char caller_pubkey_hex[65] = {0};
            int auth_rc = 0;
            cJSON *req = NULL;

            s_frame_buf[last_len] = '\0';

            if (extract_json_id_token((const char *)s_frame_buf, id_token, sizeof(id_token)) != 0) {
                strncpy(id_token, "null", sizeof(id_token) - 1);
            }

            req = cJSON_Parse((const char *)s_frame_buf);

            if (req == NULL ||
                extract_json_string_field((const char *)s_frame_buf, "method", method, sizeof(method)) != 0) {
                snprintf(
                    s_response_buf,
                    sizeof(s_response_buf),
                    "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":-32600,\"message\":\"invalid request\"}}",
                    id_token);
            } else {
#if AUTH_REQUIRED
                auth_rc = auth_envelope_verify(req, method, caller_pubkey_hex);
#else
                auth_rc = 0;
                strncpy(caller_pubkey_hex, "anon", sizeof(caller_pubkey_hex) - 1);
#endif

                if (auth_rc != 0) {
                    const char *msg = "auth envelope required";
                    if (auth_rc == AUTH_ERR_ENVELOPE_MALFORMED) msg = "auth envelope malformed";
                    else if (auth_rc == AUTH_ERR_BODY_MISMATCH) msg = "auth body mismatch";
                    else if (auth_rc == AUTH_ERR_SIGNATURE_INVALID) msg = "auth signature invalid";
                    else if (auth_rc == AUTH_ERR_KIND_INVALID) msg = "auth kind invalid";
                    else if (auth_rc == AUTH_ERR_REPLAY_DETECTED) msg = "auth replay detected";

                    snprintf(
                        s_response_buf,
                        sizeof(s_response_buf),
                        "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":%d,\"message\":\"%s\"}}",
                        id_token, auth_rc, msg);
                } else if (strcmp(method, "get_public_key") == 0) {
                    cJSON *params = cJSON_GetObjectItemCaseSensitive(req, "params");
                    uint32_t nostr_index = 0;
                    uint8_t req_privkey[32];
                    char req_pubkey_hex[65];

                    memset(req_privkey, 0, sizeof(req_privkey));
                    memset(req_pubkey_hex, 0, sizeof(req_pubkey_hex));

                    if (!cJSON_IsArray(params) ||
                        parse_nostr_index_from_params(params, &nostr_index) != 0 ||
                        derive_request_key(nostr_index, req_privkey, req_pubkey_hex) != 0) {
                        snprintf(
                            s_response_buf,
                            sizeof(s_response_buf),
                            "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":-32602,\"message\":\"invalid params\"}}",
                            id_token);
                    } else {
                        approval_decision_t decision = prompt_non_sign_approval(caller_pubkey_hex, 27235, "get_public_key");
                        if (decision == APPROVAL_ONCE || decision == APPROVAL_ALWAYS) {
                            ui_add_event("get_public_key", caller_pubkey_hex, "approved");
                            snprintf(
                                s_response_buf,
                                sizeof(s_response_buf),
                                "{\"jsonrpc\":\"2.0\",\"id\":%s,\"result\":\"%s\"}",
                                id_token,
                                req_pubkey_hex);
                        } else if (decision == APPROVAL_DENY) {
                            snprintf(
                                s_response_buf,
                                sizeof(s_response_buf),
                                "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":-32000,\"message\":\"denied by user\"}}",
                                id_token);
                        } else {
                            snprintf(
                                s_response_buf,
                                sizeof(s_response_buf),
                                "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":-32001,\"message\":\"approval timeout\"}}",
                                id_token);
                        }
                    }

                    secure_memzero(req_privkey, sizeof(req_privkey));
                } else if (strcmp(method, "sign_event") == 0) {
                    cJSON *params = cJSON_GetObjectItemCaseSensitive(req, "params");
                    cJSON *event_in = NULL;
                    uint32_t nostr_index = 0;
                    uint8_t req_privkey[32];
                    char req_pubkey_hex[65];

                    memset(req_privkey, 0, sizeof(req_privkey));
                    memset(req_pubkey_hex, 0, sizeof(req_pubkey_hex));

                    if (cJSON_IsArray(params) && cJSON_GetArraySize(params) > 0) {
                        event_in = cJSON_GetArrayItem(params, 0);
                    }

                    if (event_in != NULL && cJSON_IsObject(event_in) &&
                        parse_nostr_index_from_params(params, &nostr_index) == 0 &&
                        derive_request_key(nostr_index, req_privkey, req_pubkey_hex) == 0) {
                        approval_decision_t decision = APPROVAL_ALWAYS;
                        int event_kind = 1;
                        char event_extra[32];
                        const cJSON *kind_item = cJSON_GetObjectItemCaseSensitive(event_in, "kind");

                        if (cJSON_IsNumber(kind_item)) {
                            event_kind = kind_item->valueint;
                        }

                        if (!s_always_allow_sign) {
                            show_approval_prompt_with_caller(event_in, caller_pubkey_hex);
                            decision = wait_for_approval(APPROVAL_TIMEOUT_MS);
                            if (decision == APPROVAL_ALWAYS) {
                                s_always_allow_sign = true;
                            }
                        }

                        if (decision == APPROVAL_ONCE || decision == APPROVAL_ALWAYS) {
                            snprintf(event_extra, sizeof(event_extra), "kind:%d", event_kind);
                            ui_add_event("sign_event", caller_pubkey_hex, event_extra);
                        }

                        if ((decision == APPROVAL_ONCE || decision == APPROVAL_ALWAYS) &&
                            build_signed_event_json(event_in, req_pubkey_hex, req_privkey, s_signed_event_buf, sizeof(s_signed_event_buf)) == 0) {
                            snprintf(
                                s_response_buf,
                                sizeof(s_response_buf),
                                "{\"jsonrpc\":\"2.0\",\"id\":%s,\"result\":%s}",
                                id_token,
                                s_signed_event_buf);
                        } else if (decision == APPROVAL_DENY) {
                            snprintf(
                                s_response_buf,
                                sizeof(s_response_buf),
                                "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":-32000,\"message\":\"denied by user\"}}",
                                id_token);
                        } else if (decision == APPROVAL_TIMEOUT) {
                            snprintf(
                                s_response_buf,
                                sizeof(s_response_buf),
                                "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":-32001,\"message\":\"approval timeout\"}}",
                                id_token);
                        } else {
                            snprintf(
                                s_response_buf,
                                sizeof(s_response_buf),
                                "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":-32603,\"message\":\"internal error\"}}",
                                id_token);
                        }
                    } else {
                        snprintf(
                            s_response_buf,
                            sizeof(s_response_buf),
                            "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":-32602,\"message\":\"invalid params\"}}",
                            id_token);
                    }

                    secure_memzero(req_privkey, sizeof(req_privkey));
                } else if (strcmp(method, "nip04_encrypt") == 0) {
                    cJSON *params = cJSON_GetObjectItemCaseSensitive(req, "params");
                    const char *peer_hex = NULL;
                    const char *plaintext = NULL;
                    uint32_t nostr_index = 0;
                    uint8_t peer_pubkey[32];
                    uint8_t req_privkey[32];
                    char req_pubkey_hex[65];

                    memset(peer_pubkey, 0, sizeof(peer_pubkey));
                    memset(req_privkey, 0, sizeof(req_privkey));
                    memset(req_pubkey_hex, 0, sizeof(req_pubkey_hex));
                    memset(s_encrypt_buf, 0, sizeof(s_encrypt_buf));

                    if (parse_peer_and_message_params(params, &peer_hex, &plaintext, &nostr_index) != 0 ||
                        hex_to_bytes(peer_hex, peer_pubkey, sizeof(peer_pubkey)) != 0 ||
                        derive_request_key(nostr_index, req_privkey, req_pubkey_hex) != 0) {
                        snprintf(
                            s_response_buf,
                            sizeof(s_response_buf),
                            "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":-32602,\"message\":\"invalid params\"}}",
                            id_token);
                    } else {
                        approval_decision_t decision = prompt_non_sign_approval(caller_pubkey_hex, 27235, "nip04_encrypt");
                        if (decision == APPROVAL_ONCE || decision == APPROVAL_ALWAYS) {
                            ui_add_event("nip04_encrypt", caller_pubkey_hex, "approved");
                            if (nostr_nip04_encrypt(req_privkey, peer_pubkey, plaintext,
                                                    s_encrypt_buf, sizeof(s_encrypt_buf)) != NOSTR_SUCCESS) {
                                snprintf(
                                    s_response_buf,
                                    sizeof(s_response_buf),
                                    "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":-32603,\"message\":\"nip04 encryption failed\"}}",
                                    id_token);
                            } else {
                                snprintf(
                                    s_response_buf,
                                    sizeof(s_response_buf),
                                    "{\"jsonrpc\":\"2.0\",\"id\":%s,\"result\":\"%s\"}",
                                    id_token,
                                    s_encrypt_buf);
                            }
                        } else if (decision == APPROVAL_DENY) {
                            snprintf(
                                s_response_buf,
                                sizeof(s_response_buf),
                                "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":-32000,\"message\":\"denied by user\"}}",
                                id_token);
                        } else {
                            snprintf(
                                s_response_buf,
                                sizeof(s_response_buf),
                                "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":-32001,\"message\":\"approval timeout\"}}",
                                id_token);
                        }
                    }

                    secure_memzero(req_privkey, sizeof(req_privkey));
                    secure_memzero(peer_pubkey, sizeof(peer_pubkey));
                } else if (strcmp(method, "nip44_encrypt") == 0) {
                    cJSON *params = cJSON_GetObjectItemCaseSensitive(req, "params");
                    const char *peer_hex = NULL;
                    const char *plaintext = NULL;
                    uint32_t nostr_index = 0;
                    uint8_t peer_pubkey[32];
                    uint8_t req_privkey[32];
                    char req_pubkey_hex[65];

                    memset(peer_pubkey, 0, sizeof(peer_pubkey));
                    memset(req_privkey, 0, sizeof(req_privkey));
                    memset(req_pubkey_hex, 0, sizeof(req_pubkey_hex));
                    memset(s_encrypt_buf, 0, sizeof(s_encrypt_buf));

                    if (parse_peer_and_message_params(params, &peer_hex, &plaintext, &nostr_index) != 0 ||
                        hex_to_bytes(peer_hex, peer_pubkey, sizeof(peer_pubkey)) != 0 ||
                        derive_request_key(nostr_index, req_privkey, req_pubkey_hex) != 0) {
                        snprintf(
                            s_response_buf,
                            sizeof(s_response_buf),
                            "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":-32602,\"message\":\"invalid params\"}}",
                            id_token);
                    } else {
                        approval_decision_t decision = prompt_non_sign_approval(caller_pubkey_hex, 27235, "nip44_encrypt");
                        if (decision == APPROVAL_ONCE || decision == APPROVAL_ALWAYS) {
                            ui_add_event("nip44_encrypt", caller_pubkey_hex, "approved");
                            if (nostr_nip44_encrypt(req_privkey, peer_pubkey, plaintext,
                                                    s_encrypt_buf, sizeof(s_encrypt_buf)) != NOSTR_SUCCESS) {
                                snprintf(
                                    s_response_buf,
                                    sizeof(s_response_buf),
                                    "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":-32603,\"message\":\"nip44 encryption failed\"}}",
                                    id_token);
                            } else {
                                snprintf(
                                    s_response_buf,
                                    sizeof(s_response_buf),
                                    "{\"jsonrpc\":\"2.0\",\"id\":%s,\"result\":\"%s\"}",
                                    id_token,
                                    s_encrypt_buf);
                            }
                        } else if (decision == APPROVAL_DENY) {
                            snprintf(
                                s_response_buf,
                                sizeof(s_response_buf),
                                "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":-32000,\"message\":\"denied by user\"}}",
                                id_token);
                        } else {
                            snprintf(
                                s_response_buf,
                                sizeof(s_response_buf),
                                "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":-32001,\"message\":\"approval timeout\"}}",
                                id_token);
                        }
                    }

                    secure_memzero(req_privkey, sizeof(req_privkey));
                    secure_memzero(peer_pubkey, sizeof(peer_pubkey));
                } else if (strcmp(method, "nip04_decrypt") == 0) {
                    cJSON *params = cJSON_GetObjectItemCaseSensitive(req, "params");
                    const char *peer_hex = NULL;
                    const char *ciphertext = NULL;
                    uint32_t nostr_index = 0;
                    uint8_t peer_pubkey[32];
                    uint8_t req_privkey[32];
                    char req_pubkey_hex[65];

                    memset(peer_pubkey, 0, sizeof(peer_pubkey));
                    memset(req_privkey, 0, sizeof(req_privkey));
                    memset(req_pubkey_hex, 0, sizeof(req_pubkey_hex));
                    memset(s_encrypt_buf, 0, sizeof(s_encrypt_buf));

                    if (parse_peer_and_message_params(params, &peer_hex, &ciphertext, &nostr_index) != 0 ||
                        hex_to_bytes(peer_hex, peer_pubkey, sizeof(peer_pubkey)) != 0 ||
                        derive_request_key(nostr_index, req_privkey, req_pubkey_hex) != 0) {
                        snprintf(
                            s_response_buf,
                            sizeof(s_response_buf),
                            "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":-32602,\"message\":\"invalid params\"}}",
                            id_token);
                    } else {
                        approval_decision_t decision = prompt_non_sign_approval(caller_pubkey_hex, 27235, "nip04_decrypt");
                        if (decision == APPROVAL_ONCE || decision == APPROVAL_ALWAYS) {
                            ui_add_event("nip04_decrypt", caller_pubkey_hex, "approved");
                            if (nostr_nip04_decrypt(req_privkey, peer_pubkey, ciphertext,
                                                    s_encrypt_buf, sizeof(s_encrypt_buf)) != NOSTR_SUCCESS) {
                                snprintf(
                                    s_response_buf,
                                    sizeof(s_response_buf),
                                    "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":-32603,\"message\":\"nip04 decryption failed\"}}",
                                    id_token);
                            } else {
                                cJSON *resp_obj = cJSON_CreateObject();
                                if (resp_obj == NULL) {
                                    snprintf(
                                        s_response_buf,
                                        sizeof(s_response_buf),
                                        "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":-32603,\"message\":\"internal error\"}}",
                                        id_token);
                                } else {
                                    cJSON_AddStringToObject(resp_obj, "jsonrpc", "2.0");
                                    cJSON_AddRawToObject(resp_obj, "id", id_token);
                                    cJSON_AddStringToObject(resp_obj, "result", s_encrypt_buf);
                                    {
                                        char *resp_json = cJSON_PrintUnformatted(resp_obj);
                                        if (resp_json != NULL && strlen(resp_json) < sizeof(s_response_buf)) {
                                            memcpy(s_response_buf, resp_json, strlen(resp_json) + 1);
                                        } else {
                                            snprintf(
                                                s_response_buf,
                                                sizeof(s_response_buf),
                                                "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":-32603,\"message\":\"internal error\"}}",
                                                id_token);
                                        }
                                        if (resp_json != NULL) {
                                            cJSON_free(resp_json);
                                        }
                                    }
                                    cJSON_Delete(resp_obj);
                                }
                            }
                        } else if (decision == APPROVAL_DENY) {
                            snprintf(
                                s_response_buf,
                                sizeof(s_response_buf),
                                "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":-32000,\"message\":\"denied by user\"}}",
                                id_token);
                        } else {
                            snprintf(
                                s_response_buf,
                                sizeof(s_response_buf),
                                "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":-32001,\"message\":\"approval timeout\"}}",
                                id_token);
                        }
                    }

                    secure_memzero(req_privkey, sizeof(req_privkey));
                    secure_memzero(peer_pubkey, sizeof(peer_pubkey));
                } else if (strcmp(method, "nip44_decrypt") == 0) {
                    cJSON *params = cJSON_GetObjectItemCaseSensitive(req, "params");
                    const char *peer_hex = NULL;
                    const char *ciphertext = NULL;
                    uint32_t nostr_index = 0;
                    uint8_t peer_pubkey[32];
                    uint8_t req_privkey[32];
                    char req_pubkey_hex[65];

                    memset(peer_pubkey, 0, sizeof(peer_pubkey));
                    memset(req_privkey, 0, sizeof(req_privkey));
                    memset(req_pubkey_hex, 0, sizeof(req_pubkey_hex));
                    memset(s_encrypt_buf, 0, sizeof(s_encrypt_buf));

                    if (parse_peer_and_message_params(params, &peer_hex, &ciphertext, &nostr_index) != 0 ||
                        hex_to_bytes(peer_hex, peer_pubkey, sizeof(peer_pubkey)) != 0 ||
                        derive_request_key(nostr_index, req_privkey, req_pubkey_hex) != 0) {
                        snprintf(
                            s_response_buf,
                            sizeof(s_response_buf),
                            "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":-32602,\"message\":\"invalid params\"}}",
                            id_token);
                    } else {
                        approval_decision_t decision = prompt_non_sign_approval(caller_pubkey_hex, 27235, "nip44_decrypt");
                        if (decision == APPROVAL_ONCE || decision == APPROVAL_ALWAYS) {
                            ui_add_event("nip44_decrypt", caller_pubkey_hex, "approved");
                            if (nostr_nip44_decrypt(req_privkey, peer_pubkey, ciphertext,
                                                    s_encrypt_buf, sizeof(s_encrypt_buf)) != NOSTR_SUCCESS) {
                                snprintf(
                                    s_response_buf,
                                    sizeof(s_response_buf),
                                    "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":-32603,\"message\":\"nip44 decryption failed\"}}",
                                    id_token);
                            } else {
                                cJSON *resp_obj = cJSON_CreateObject();
                                if (resp_obj == NULL) {
                                    snprintf(
                                        s_response_buf,
                                        sizeof(s_response_buf),
                                        "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":-32603,\"message\":\"internal error\"}}",
                                        id_token);
                                } else {
                                    cJSON_AddStringToObject(resp_obj, "jsonrpc", "2.0");
                                    cJSON_AddRawToObject(resp_obj, "id", id_token);
                                    cJSON_AddStringToObject(resp_obj, "result", s_encrypt_buf);
                                    {
                                        char *resp_json = cJSON_PrintUnformatted(resp_obj);
                                        if (resp_json != NULL && strlen(resp_json) < sizeof(s_response_buf)) {
                                            memcpy(s_response_buf, resp_json, strlen(resp_json) + 1);
                                        } else {
                                            snprintf(
                                                s_response_buf,
                                                sizeof(s_response_buf),
                                                "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":-32603,\"message\":\"internal error\"}}",
                                                id_token);
                                        }
                                        if (resp_json != NULL) {
                                            cJSON_free(resp_json);
                                        }
                                    }
                                    cJSON_Delete(resp_obj);
                                }
                            }
                        } else if (decision == APPROVAL_DENY) {
                            snprintf(
                                s_response_buf,
                                sizeof(s_response_buf),
                                "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":-32000,\"message\":\"denied by user\"}}",
                                id_token);
                        } else {
                            snprintf(
                                s_response_buf,
                                sizeof(s_response_buf),
                                "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":-32001,\"message\":\"approval timeout\"}}",
                                id_token);
                        }
                    }

                    secure_memzero(req_privkey, sizeof(req_privkey));
                    secure_memzero(peer_pubkey, sizeof(peer_pubkey));
                } else {
                    snprintf(
                        s_response_buf,
                        sizeof(s_response_buf),
                        "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":-32601,\"message\":\"method not found\"}}",
                        id_token);
                }
            }

            if (req != NULL) {
                cJSON_Delete(req);
            }

            if ((!via_webusb && send_frame_stdout((const uint8_t *)s_response_buf, strlen(s_response_buf)) == 0) ||
                (via_webusb && webusb_send_frame((const uint8_t *)s_response_buf, strlen(s_response_buf)) == 0)) {
                tx_frames++;
            } else {
                ESP_LOGE(TAG, "failed to send JSON-RPC response over %s", via_webusb ? "WebUSB" : "CDC");
            }
        } else {
            snprintf(
                s_response_buf,
                sizeof(s_response_buf),
                "{\"jsonrpc\":\"2.0\",\"id\":null,\"error\":{\"code\":-32600,\"message\":\"request too large\"}}");

            if ((!via_webusb && send_frame_stdout((const uint8_t *)s_response_buf, strlen(s_response_buf)) == 0) ||
                (via_webusb && webusb_send_frame((const uint8_t *)s_response_buf, strlen(s_response_buf)) == 0)) {
                tx_frames++;
            } else {
                ESP_LOGE(TAG, "failed to send oversized-request error over %s", via_webusb ? "WebUSB" : "CDC");
            }
        }

        (void)stat1;
        (void)stat2;
        (void)rx_frames;
        (void)tx_frames;
        (void)last_len;

        /* Keep LVGL alive so approval/idle screens render after the menu exits. */
        ui_pump();
    }
}

void app_main(void)
{
    ESP_LOGI(TAG, "n_signer v%s booting...", FIRMWARE_VERSION);
    printf("n_signer booting\n");

    ESP_ERROR_CHECK(display_init());
    ESP_ERROR_CHECK(display_clear(RGB565_BLACK));

    memset(s_mnemonic, 0, sizeof(s_mnemonic));
    memset(s_seed, 0, sizeof(s_seed));
    memset(s_privkey, 0, sizeof(s_privkey));
    memset(s_pubkey, 0, sizeof(s_pubkey));
    memset(s_npub, 0, sizeof(s_npub));
    memset(s_frame_buf, 0, sizeof(s_frame_buf));
    memset(s_pubkey_hex, 0, sizeof(s_pubkey_hex));

#if CYD_DEBUG_AUTO_GENERATE
    if (ui_init_runtime() != 0) {
        ESP_ERROR_CHECK(display_clear(RGB565_BLACK));
        ESP_ERROR_CHECK(display_draw_text_centered(56, "UI INIT FAIL", RGB565_RED, RGB565_BLACK));
        return;
    }

    if (generate_mnemonic_12(s_mnemonic, sizeof(s_mnemonic)) != 0) {
        ESP_ERROR_CHECK(display_clear(RGB565_BLACK));
        ESP_ERROR_CHECK(display_draw_text_centered(56, "MN GEN FAIL", RGB565_RED, RGB565_BLACK));
        return;
    }
#else
    if (ui_run_until_working(s_mnemonic) != 0) {
        ESP_ERROR_CHECK(display_clear(RGB565_BLACK));
        ESP_ERROR_CHECK(display_draw_text_centered(56, "UI FAILED", RGB565_RED, RGB565_BLACK));
        return;
    }
#endif

    if (apply_mnemonic_and_enter_working(s_mnemonic) != 0) {
        secure_memzero(s_mnemonic, sizeof(s_mnemonic));
        return;
    }

    secure_memzero(s_mnemonic, sizeof(s_mnemonic));
    printf("usb cdc+webusb ready\n");

    /* Prevent log text from corrupting framed transport bytes on this same USB stream. */
    esp_log_level_set("*", ESP_LOG_NONE);

    buttons_init();

    if (transport_init() != 0) {
        ESP_ERROR_CHECK(display_draw_text(10, 90, "cdc init fail", RGB565_RED, RGB565_BLACK));
        while (1) {
            vTaskDelay(pdMS_TO_TICKS(1000));
        }
    }

    if (nostr_init() != NOSTR_SUCCESS) {
        ESP_ERROR_CHECK(display_draw_text(10, 90, "nostr init fail", RGB565_RED, RGB565_BLACK));
        while (1) {
            vTaskDelay(pdMS_TO_TICKS(1000));
        }
    }

    /* WebUSB temporarily disabled to preserve stable USB Serial/JTAG CDC enumeration. */
    run_signing_loop();
}
