// =========================
// storage_service.c (revisto)
// - FIX: commit não limpa pending se nvs_set falhar (limpa por-entry somente se aplicada + commit OK)
// - FIX: sync_req respeita timeout total (budget)
// - blob_query_only agora é respeitado (query não lê payload)
// - BLOB size=0 tratado
// - (upgrade opcional) adiciona setters sync (backward-compatible)
// =========================

#include "storage_service.h"

#include <string.h>
#include <stdbool.h>

#include "esp_log.h"
#include "esp_err.h"
#include "nvs.h"

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "freertos/semphr.h"

static const char *TAG = "storage_service";

typedef enum
{
    OP_SET_U8,
    OP_SET_U16,
    OP_SET_U32,
    OP_SET_STR,
    OP_SET_BLOB,

    OP_ERASE_KEY,

    OP_GET_U8,
    OP_GET_U16,
    OP_GET_U32,
    OP_GET_STR,
    OP_GET_BLOB,

    OP_FLUSH,
} storage_op_t;

typedef enum
{
    T_U8,
    T_U16,
    T_U32,
    T_STR,
    T_BLOB,
} storage_type_t;

typedef struct
{
    esp_err_t err;
    uint32_t value;                         // U8/U16/U32
    size_t len;                             // STR/BLOB: tamanho (STR sem '\0', BLOB tamanho real)
    uint8_t bytes[STORAGE_MAX_VALUE_BYTES]; // payload para STR/BLOB (até MAX)
} storage_resp_t;

typedef struct
{
    storage_op_t op;
    char ns[STORAGE_NS_MAX_LEN];
    char key[STORAGE_KEY_MAX_LEN];

    // SET/GET numéricos
    uint32_t value;

    // SET STR/BLOB: bytes e len (copiados na mensagem => seguro async)
    uint16_t len;
    uint8_t bytes[STORAGE_MAX_VALUE_BYTES];

    // GET_BLOB: se true, apenas retorna len (não lê payload)
    bool blob_query_only;

    QueueHandle_t resp_q; // opcional (GET/FLUSH/SET sync)
} storage_msg_t;

typedef struct
{
    bool used;
    bool erase;
    char ns[STORAGE_NS_MAX_LEN];
    char key[STORAGE_KEY_MAX_LEN];
    storage_type_t type;

    uint32_t value;                         // U8/U16/U32
    uint16_t len;                           // STR/BLOB (até MAX)
    uint8_t bytes[STORAGE_MAX_VALUE_BYTES]; // STR/BLOB
} pending_item_t;

static bool s_inited = false;

// Queue principal estática (evita malloc)
static StaticQueue_t s_qbuf;
static uint8_t s_qstorage[STORAGE_QUEUE_LEN * sizeof(storage_msg_t)];
static QueueHandle_t s_q = NULL;

// Pending table (evita malloc)
static pending_item_t s_pending[STORAGE_MAX_PENDING];
static size_t s_pending_count = 0;

// debounce
static bool s_dirty = false;
static TickType_t s_commit_deadline = 0;

// Sync: fila global + mutex global (1 request sync por vez)
static StaticQueue_t s_sync_qbuf;
static uint8_t s_sync_qstor[sizeof(storage_resp_t)];
static QueueHandle_t s_sync_q = NULL;

static StaticSemaphore_t s_sync_mtx_buf;
static SemaphoreHandle_t s_sync_mtx = NULL;

// -------------------- helpers --------------------

static inline esp_err_t map_not_found(esp_err_t e)
{
    return (e == ESP_ERR_NVS_NOT_FOUND) ? ESP_ERR_NOT_FOUND : e;
}

static bool safe_copy_str(char *dst, size_t dst_sz, const char *src)
{
    if (!dst || dst_sz == 0 || !src)
        return false;
    size_t n = strnlen(src, dst_sz);
    if (n >= dst_sz)
        return false;
    memcpy(dst, src, n);
    dst[n] = '\0';
    return true;
}

static TickType_t ticks_elapsed(TickType_t start, TickType_t now)
{
    // FreeRTOS ticks wrap: subtração unsigned é ok
    return now - start;
}

static TickType_t budget_left(TickType_t start, TickType_t to)
{
    if (to == portMAX_DELAY)
        return portMAX_DELAY;

    TickType_t now = xTaskGetTickCount();
    TickType_t elapsed = ticks_elapsed(start, now);
    if (elapsed >= to)
        return 0;
    return to - elapsed;
}

static void pending_clear_slot(int idx)
{
    if (idx < 0 || idx >= (int)STORAGE_MAX_PENDING)
        return;

    if (s_pending[idx].used)
    {
        s_pending[idx].used = false;
        s_pending[idx].erase = false;
        s_pending[idx].len = 0;
        s_pending[idx].value = 0;
        memset(s_pending[idx].bytes, 0, sizeof(s_pending[idx].bytes));
        s_pending[idx].ns[0] = '\0';
        s_pending[idx].key[0] = '\0';

        if (s_pending_count > 0)
            s_pending_count--;
    }
}

static void mark_dirty(void)
{
    s_dirty = true;
    s_commit_deadline = xTaskGetTickCount() + pdMS_TO_TICKS(STORAGE_COMMIT_DEBOUNCE_MS);
}

static int find_pending(const char *ns, const char *key)
{
    for (int i = 0; i < (int)STORAGE_MAX_PENDING; ++i)
    {
        if (!s_pending[i].used)
            continue;
        if (strncmp(s_pending[i].ns, ns, STORAGE_NS_MAX_LEN) == 0 &&
            strncmp(s_pending[i].key, key, STORAGE_KEY_MAX_LEN) == 0)
        {
            return i;
        }
    }
    return -1;
}

static bool pending_is_erased(const char *ns, const char *key)
{
    int idx = find_pending(ns, key);
    if (idx < 0)
        return false;
    return s_pending[idx].used && s_pending[idx].erase;
}

static int alloc_pending_slot(void)
{
    for (int i = 0; i < (int)STORAGE_MAX_PENDING; ++i)
    {
        if (!s_pending[i].used)
            return i;
    }
    return -1;
}

static esp_err_t ensure_pending_entry(int *out_idx, const char *ns, const char *key)
{
    int idx = find_pending(ns, key);
    if (idx < 0)
    {
        idx = alloc_pending_slot();
        if (idx < 0)
            return ESP_ERR_NO_MEM;

        memset(&s_pending[idx], 0, sizeof(s_pending[idx]));
        s_pending[idx].used = true;

        if (!safe_copy_str(s_pending[idx].ns, sizeof(s_pending[idx].ns), ns))
        {
            s_pending[idx].used = false;
            return ESP_ERR_INVALID_ARG;
        }
        if (!safe_copy_str(s_pending[idx].key, sizeof(s_pending[idx].key), key))
        {
            s_pending[idx].used = false;
            return ESP_ERR_INVALID_ARG;
        }
        s_pending_count++;
    }
    if (out_idx)
        *out_idx = idx;
    return ESP_OK;
}

static esp_err_t pending_set_num(const char *ns, const char *key, storage_type_t type, uint32_t v)
{
    int idx = -1;
    esp_err_t err = ensure_pending_entry(&idx, ns, key);
    if (err != ESP_OK)
        return err;

    s_pending[idx].erase = false;
    s_pending[idx].type = type;
    s_pending[idx].value = v;
    s_pending[idx].len = 0;

    mark_dirty();
    return ESP_OK;
}

static esp_err_t pending_set_bytes(const char *ns, const char *key, storage_type_t type,
                                   const uint8_t *bytes, uint16_t len)
{
    if (!bytes && len > 0)
        return ESP_ERR_INVALID_ARG;
    if (len > STORAGE_MAX_VALUE_BYTES)
        return ESP_ERR_INVALID_SIZE;

    int idx = -1;
    esp_err_t err = ensure_pending_entry(&idx, ns, key);
    if (err != ESP_OK)
        return err;

    s_pending[idx].erase = false;
    s_pending[idx].type = type;
    s_pending[idx].value = 0;
    s_pending[idx].len = len;

    if (len > 0)
        memcpy(s_pending[idx].bytes, bytes, len);
    if (len < STORAGE_MAX_VALUE_BYTES)
        memset(&s_pending[idx].bytes[len], 0, STORAGE_MAX_VALUE_BYTES - len);

    mark_dirty();
    return ESP_OK;
}

static esp_err_t pending_erase(const char *ns, const char *key)
{
    int idx = -1;
    esp_err_t err = ensure_pending_entry(&idx, ns, key);
    if (err != ESP_OK)
        return err;

    s_pending[idx].erase = true;
    mark_dirty();
    return ESP_OK;
}

static bool pending_get_num(const char *ns, const char *key, storage_type_t type, uint32_t *out)
{
    int idx = find_pending(ns, key);
    if (idx < 0 || !s_pending[idx].used || s_pending[idx].erase || s_pending[idx].type != type)
        return false;

    if (out)
        *out = s_pending[idx].value;
    return true;
}

static bool pending_get_bytes(const char *ns, const char *key, storage_type_t type,
                              uint8_t *out, size_t out_sz, uint16_t *out_len)
{
    int idx = find_pending(ns, key);
    if (idx < 0 || !s_pending[idx].used || s_pending[idx].erase || s_pending[idx].type != type)
        return false;

    uint16_t len = s_pending[idx].len;

    if (out_len)
        *out_len = len;

    if (out)
    {
        size_t n = (len <= out_sz) ? (size_t)len : out_sz;
        if (n > 0)
            memcpy(out, s_pending[idx].bytes, n);
    }
    return true;
}

// -------------------- Commit (FIX: limpa por-entry) --------------------

// Aplica uma pending entry no handle já aberto.
// Retorna ESP_OK se a operação foi aplicada com sucesso (ou erase de key inexistente).
static esp_err_t apply_pending_item(nvs_handle_t h, const pending_item_t *it)
{
    if (!it || !it->used)
        return ESP_ERR_INVALID_ARG;

    if (it->erase)
    {
        esp_err_t e = nvs_erase_key(h, it->key);
        if (e == ESP_ERR_NVS_NOT_FOUND)
            return ESP_OK; // semântica: apagar key que não existe não é erro
        return e;
    }

    switch (it->type)
    {
    case T_U8:
        return nvs_set_u8(h, it->key, (uint8_t)(it->value & 0xFF));
    case T_U16:
        return nvs_set_u16(h, it->key, (uint16_t)(it->value & 0xFFFF));
    case T_U32:
        return nvs_set_u32(h, it->key, it->value);

    case T_STR:
    {
        // garantir null-termination para nvs_set_str
        uint16_t len = it->len;
        if (len > STORAGE_MAX_VALUE_BYTES)
            return ESP_ERR_INVALID_SIZE;

        char tmp[STORAGE_MAX_VALUE_BYTES + 1];
        if (len > 0)
            memcpy(tmp, it->bytes, len);
        tmp[len] = '\0';
        return nvs_set_str(h, it->key, tmp);
    }

    case T_BLOB:
    {
        uint16_t len = it->len;
        if (len > STORAGE_MAX_VALUE_BYTES)
            return ESP_ERR_INVALID_SIZE;
        return nvs_set_blob(h, it->key, it->bytes, (size_t)len);
    }

    default:
        return ESP_ERR_INVALID_STATE;
    }
}

// Commit: agrupa por namespace e faz 1 commit por namespace
static esp_err_t commit_all_pending(void)
{
    if (s_pending_count == 0)
    {
        s_dirty = false;
        return ESP_OK;
    }

    esp_err_t overall = ESP_OK;

    // Lista de namespaces únicos (sem heap)
    char ns_list[STORAGE_MAX_PENDING][STORAGE_NS_MAX_LEN];
    int ns_count = 0;

    for (int i = 0; i < (int)STORAGE_MAX_PENDING; ++i)
    {
        if (!s_pending[i].used)
            continue;

        bool seen = false;
        for (int j = 0; j < ns_count; ++j)
        {
            if (strncmp(ns_list[j], s_pending[i].ns, STORAGE_NS_MAX_LEN) == 0)
            {
                seen = true;
                break;
            }
        }
        if (!seen && ns_count < (int)STORAGE_MAX_PENDING)
        {
            (void)safe_copy_str(ns_list[ns_count], sizeof(ns_list[ns_count]), s_pending[i].ns);
            ns_count++;
        }
    }

    for (int n = 0; n < ns_count; ++n)
    {
        const char *ns = ns_list[n];
        nvs_handle_t h;

        esp_err_t err_open = nvs_open(ns, NVS_READWRITE, &h);
        if (err_open != ESP_OK)
        {
            ESP_LOGE(TAG, "nvs_open('%s') failed: %s", ns, esp_err_to_name(err_open));
            overall = err_open;
            continue;
        }

        // Marca quais slots foram aplicados com sucesso para limpar apenas esses
        bool clear_mask[STORAGE_MAX_PENDING];
        memset(clear_mask, 0, sizeof(clear_mask));

        bool any_applied = false;
        bool all_applied_ok = true;

        for (int i = 0; i < (int)STORAGE_MAX_PENDING; ++i)
        {
            if (!s_pending[i].used)
                continue;
            if (strncmp(s_pending[i].ns, ns, STORAGE_NS_MAX_LEN) != 0)
                continue;

            any_applied = true;

            esp_err_t e = apply_pending_item(h, &s_pending[i]);
            if (e == ESP_OK)
            {
                clear_mask[i] = true; // candidato a limpar após commit OK
            }
            else
            {
                all_applied_ok = false;
                overall = e;
                ESP_LOGE(TAG, "apply %s/%s failed: %s", ns, s_pending[i].key, esp_err_to_name(e));
                // Mantém entry no pending para retry (ou debug). Não marca para limpar.
            }
        }

        esp_err_t err_commit = ESP_OK;
        if (any_applied)
        {
            err_commit = nvs_commit(h);
            if (err_commit != ESP_OK)
            {
                overall = err_commit;
                ESP_LOGE(TAG, "commit('%s') failed: %s", ns, esp_err_to_name(err_commit));
            }
        }

        nvs_close(h);

        // FIX: só limpa entries que:
        //  - foram aplicadas com sucesso (clear_mask[i]=true)
        //  - E o commit do namespace foi OK
        if (err_commit == ESP_OK)
        {
            for (int i = 0; i < (int)STORAGE_MAX_PENDING; ++i)
            {
                if (clear_mask[i])
                    pending_clear_slot(i);
            }
        }
        else
        {
            // Se commit falhou, não limpamos nada (nem as que aplicaram),
            // para garantir retry consistente.
        }

        (void)all_applied_ok; // mantido se quiseres usar para métricas no futuro
    }

    if (s_pending_count == 0)
    {
        s_dirty = false;
    }
    else
    {
        // ainda há itens -> rearmar debounce para tentar novamente
        mark_dirty();
    }

    return overall;
}

// -------------------- Read helpers (cópia na resposta) --------------------

static esp_err_t nvs_read_num(storage_type_t type, const char *ns, const char *key, uint32_t *out)
{
    if (pending_is_erased(ns, key))
        return ESP_ERR_NOT_FOUND;

    uint32_t pv = 0;
    if (pending_get_num(ns, key, type, &pv))
    {
        if (out)
            *out = pv;
        return ESP_OK;
    }

    nvs_handle_t h;
    esp_err_t err = nvs_open(ns, NVS_READONLY, &h);
    if (err != ESP_OK)
        return map_not_found(err);

    switch (type)
    {
    case T_U8:
    {
        uint8_t v8 = 0;
        err = nvs_get_u8(h, key, &v8);
        if (err == ESP_OK && out)
            *out = v8;
        break;
    }
    case T_U16:
    {
        uint16_t v16 = 0;
        err = nvs_get_u16(h, key, &v16);
        if (err == ESP_OK && out)
            *out = v16;
        break;
    }
    case T_U32:
    {
        uint32_t v32 = 0;
        err = nvs_get_u32(h, key, &v32);
        if (err == ESP_OK && out)
            *out = v32;
        break;
    }
    default:
        err = ESP_ERR_INVALID_ARG;
        break;
    }

    nvs_close(h);
    return map_not_found(err);
}

static esp_err_t nvs_read_str_to_resp(const char *ns, const char *key, storage_resp_t *resp)
{
    if (!resp)
        return ESP_ERR_INVALID_ARG;

    resp->len = 0;

    if (pending_is_erased(ns, key))
        return ESP_ERR_NOT_FOUND;

    uint16_t plen = 0;
    if (pending_get_bytes(ns, key, T_STR, resp->bytes, STORAGE_MAX_VALUE_BYTES, &plen))
    {
        resp->len = (size_t)plen;
        return ESP_OK;
    }

    nvs_handle_t h;
    esp_err_t err = nvs_open(ns, NVS_READONLY, &h);
    if (err != ESP_OK)
        return map_not_found(err);

    // 1) query required size (inclui '\0')
    size_t req = 0;
    err = nvs_get_str(h, key, NULL, &req);
    if (err != ESP_OK)
    {
        nvs_close(h);
        return map_not_found(err);
    }

    if (req == 0)
    {
        nvs_close(h);
        resp->len = 0;
        return ESP_OK;
    }

    size_t str_len = req - 1; // sem '\0'
    resp->len = str_len;

    if (str_len > STORAGE_MAX_VALUE_BYTES)
    {
        nvs_close(h);
        return ESP_ERR_NVS_INVALID_LENGTH;
    }

    // 2) read
    char tmp[STORAGE_MAX_VALUE_BYTES + 1];
    size_t tmp_sz = sizeof(tmp);
    err = nvs_get_str(h, key, tmp, &tmp_sz);
    nvs_close(h);

    if (err != ESP_OK)
        return map_not_found(err);

    size_t n = strnlen(tmp, STORAGE_MAX_VALUE_BYTES);
    memcpy(resp->bytes, tmp, n);
    resp->len = n;
    return ESP_OK;
}

static esp_err_t nvs_query_blob_len(const char *ns, const char *key, size_t *out_len)
{
    if (!out_len)
        return ESP_ERR_INVALID_ARG;

    if (pending_is_erased(ns, key))
        return ESP_ERR_NOT_FOUND;

    uint16_t plen = 0;
    if (pending_get_bytes(ns, key, T_BLOB, NULL, 0, &plen))
    {
        *out_len = (size_t)plen;
        return ESP_OK;
    }

    nvs_handle_t h;
    esp_err_t err = nvs_open(ns, NVS_READONLY, &h);
    if (err != ESP_OK)
        return map_not_found(err);

    size_t req = 0;
    err = nvs_get_blob(h, key, NULL, &req);
    nvs_close(h);

    if (err != ESP_OK)
        return map_not_found(err);

    *out_len = req;
    return ESP_OK;
}

static esp_err_t nvs_read_blob_to_resp(const char *ns, const char *key, storage_resp_t *resp)
{
    if (!resp)
        return ESP_ERR_INVALID_ARG;

    resp->len = 0;

    if (pending_is_erased(ns, key))
        return ESP_ERR_NOT_FOUND;

    uint16_t plen = 0;
    if (pending_get_bytes(ns, key, T_BLOB, resp->bytes, STORAGE_MAX_VALUE_BYTES, &plen))
    {
        resp->len = (size_t)plen;
        return ESP_OK;
    }

    nvs_handle_t h;
    esp_err_t err = nvs_open(ns, NVS_READONLY, &h);
    if (err != ESP_OK)
        return map_not_found(err);

    // query size
    size_t req = 0;
    err = nvs_get_blob(h, key, NULL, &req);
    if (err != ESP_OK)
    {
        nvs_close(h);
        if (err == ESP_ERR_NVS_NOT_FOUND)
            resp->len = 0;
        return map_not_found(err);
    }

    resp->len = req;

    // size == 0 => nada a ler
    if (req == 0)
    {
        nvs_close(h);
        return ESP_OK;
    }

    // Se maior que o máximo, não lemos payload
    if (req > STORAGE_MAX_VALUE_BYTES)
    {
        nvs_close(h);
        return ESP_OK;
    }

    // read payload
    size_t tmp = req;
    err = nvs_get_blob(h, key, resp->bytes, &tmp);
    nvs_close(h);

    if (err != ESP_OK)
        return map_not_found(err);

    resp->len = tmp;
    return ESP_OK;
}

// -------------------- Task --------------------

static void storage_task(void *arg)
{
    (void)arg;
    storage_msg_t msg;

    while (true)
    {
        TickType_t now = xTaskGetTickCount();

        TickType_t wait = portMAX_DELAY;
        if (s_dirty)
        {
            if (now >= s_commit_deadline)
            {
                (void)commit_all_pending();
                continue;
            }
            else
            {
                wait = s_commit_deadline - now;
            }
        }

        if (xQueueReceive(s_q, &msg, wait) != pdTRUE)
        {
            if (s_dirty && xTaskGetTickCount() >= s_commit_deadline)
            {
                (void)commit_all_pending();
            }
            continue;
        }

        storage_resp_t resp;
        memset(&resp, 0, sizeof(resp));
        resp.err = ESP_OK;

        switch (msg.op)
        {
        case OP_SET_U8:
            resp.err = pending_set_num(msg.ns, msg.key, T_U8, msg.value & 0xFF);
            break;
        case OP_SET_U16:
            resp.err = pending_set_num(msg.ns, msg.key, T_U16, msg.value & 0xFFFF);
            break;
        case OP_SET_U32:
            resp.err = pending_set_num(msg.ns, msg.key, T_U32, msg.value);
            break;

        case OP_SET_STR:
            resp.err = pending_set_bytes(msg.ns, msg.key, T_STR, msg.bytes, msg.len);
            break;

        case OP_SET_BLOB:
            resp.err = pending_set_bytes(msg.ns, msg.key, T_BLOB, msg.bytes, msg.len);
            break;

        case OP_ERASE_KEY:
            resp.err = pending_erase(msg.ns, msg.key);
            break;

        case OP_GET_U8:
            resp.err = nvs_read_num(T_U8, msg.ns, msg.key, &resp.value);
            break;
        case OP_GET_U16:
            resp.err = nvs_read_num(T_U16, msg.ns, msg.key, &resp.value);
            break;
        case OP_GET_U32:
            resp.err = nvs_read_num(T_U32, msg.ns, msg.key, &resp.value);
            break;

        case OP_GET_STR:
            resp.err = nvs_read_str_to_resp(msg.ns, msg.key, &resp);
            break;

        case OP_GET_BLOB:
            if (msg.blob_query_only)
            {
                size_t len = 0;
                resp.err = nvs_query_blob_len(msg.ns, msg.key, &len);
                resp.len = (resp.err == ESP_OK) ? len : 0;
            }
            else
            {
                // query+read (se couber)
                resp.err = nvs_read_blob_to_resp(msg.ns, msg.key, &resp);
            }
            break;

        case OP_FLUSH:
            resp.err = commit_all_pending();
            break;

        default:
            resp.err = ESP_ERR_INVALID_ARG;
            break;
        }

        if (msg.resp_q)
        {
            (void)xQueueSend(msg.resp_q, &resp, 0);
        }
    }
}

// -------------------- Public API --------------------

esp_err_t storage_service_init(void)
{
    if (s_inited)
        return ESP_OK;

    s_q = xQueueCreateStatic(STORAGE_QUEUE_LEN, sizeof(storage_msg_t), s_qstorage, &s_qbuf);
    if (!s_q)
        return ESP_ERR_NO_MEM;

    s_sync_q = xQueueCreateStatic(1, sizeof(storage_resp_t), s_sync_qstor, &s_sync_qbuf);
    if (!s_sync_q)
        return ESP_ERR_NO_MEM;

    s_sync_mtx = xSemaphoreCreateMutexStatic(&s_sync_mtx_buf);
    if (!s_sync_mtx)
        return ESP_ERR_NO_MEM;

    memset(s_pending, 0, sizeof(s_pending));
    s_pending_count = 0;
    s_dirty = false;

    BaseType_t ok = xTaskCreate(storage_task, "storage", 8192, NULL, 5, NULL);
    if (ok != pdPASS)
        return ESP_ERR_NO_MEM;

    s_inited = true;
    ESP_LOGI(TAG, "storage_service init OK (queue=%d pending=%d debounce=%dms max_bytes=%d)",
             STORAGE_QUEUE_LEN, STORAGE_MAX_PENDING, STORAGE_COMMIT_DEBOUNCE_MS, STORAGE_MAX_VALUE_BYTES);
    return ESP_OK;
}

static esp_err_t send_msg(const storage_msg_t *m, TickType_t to)
{
    if (!s_inited || !s_q)
        return ESP_ERR_INVALID_STATE;
    return (xQueueSend(s_q, m, to) == pdTRUE) ? ESP_OK : ESP_ERR_TIMEOUT;
}

// -------------------- sync core (FIX: budget timeout) --------------------

static esp_err_t sync_req(const storage_msg_t *req, storage_resp_t *out_resp, TickType_t to)
{
    if (!s_inited || !s_sync_q || !s_sync_mtx)
        return ESP_ERR_INVALID_STATE;

    TickType_t start = xTaskGetTickCount();

    TickType_t b = budget_left(start, to);
    if (b == 0)
        return ESP_ERR_TIMEOUT;

    if (xSemaphoreTake(s_sync_mtx, b) != pdTRUE)
        return ESP_ERR_TIMEOUT;

    // limpar resposta anterior
    xQueueReset(s_sync_q);

    storage_msg_t m = *req;
    m.resp_q = s_sync_q;

    b = budget_left(start, to);
    if (b == 0)
    {
        xSemaphoreGive(s_sync_mtx);
        return ESP_ERR_TIMEOUT;
    }

    esp_err_t err = send_msg(&m, b);
    if (err != ESP_OK)
    {
        xSemaphoreGive(s_sync_mtx);
        return err;
    }

    storage_resp_t r;
    memset(&r, 0, sizeof(r));

    b = budget_left(start, to);
    if (b == 0)
    {
        xSemaphoreGive(s_sync_mtx);
        return ESP_ERR_TIMEOUT;
    }

    if (xQueueReceive(s_sync_q, &r, b) != pdTRUE)
    {
        xSemaphoreGive(s_sync_mtx);
        return ESP_ERR_TIMEOUT;
    }

    if (out_resp)
        *out_resp = r;

    xSemaphoreGive(s_sync_mtx);
    return r.err;
}

// -------------------- async setters --------------------

static esp_err_t set_async_num(storage_op_t op, const char *ns, const char *key, uint32_t v)
{
    storage_msg_t m;
    memset(&m, 0, sizeof(m));
    m.op = op;
    if (!safe_copy_str(m.ns, sizeof(m.ns), ns))
        return ESP_ERR_INVALID_ARG;
    if (!safe_copy_str(m.key, sizeof(m.key), key))
        return ESP_ERR_INVALID_ARG;
    m.value = v;
    return send_msg(&m, 0);
}

esp_err_t storage_set_u8_async(const char *ns, const char *key, uint8_t v)
{
    return set_async_num(OP_SET_U8, ns, key, v);
}

esp_err_t storage_set_u16_async(const char *ns, const char *key, uint16_t v)
{
    return set_async_num(OP_SET_U16, ns, key, v);
}

esp_err_t storage_set_u32_async(const char *ns, const char *key, uint32_t v)
{
    return set_async_num(OP_SET_U32, ns, key, v);
}

esp_err_t storage_set_str_async(const char *ns, const char *key, const char *str)
{
    if (!str)
        return ESP_ERR_INVALID_ARG;

    size_t len = strnlen(str, STORAGE_MAX_VALUE_BYTES + 1);
    if (len > STORAGE_MAX_VALUE_BYTES)
        return ESP_ERR_INVALID_SIZE;

    storage_msg_t m;
    memset(&m, 0, sizeof(m));
    m.op = OP_SET_STR;
    if (!safe_copy_str(m.ns, sizeof(m.ns), ns))
        return ESP_ERR_INVALID_ARG;
    if (!safe_copy_str(m.key, sizeof(m.key), key))
        return ESP_ERR_INVALID_ARG;

    m.len = (uint16_t)len;
    if (len > 0)
        memcpy(m.bytes, str, len);
    if (len < STORAGE_MAX_VALUE_BYTES)
        memset(&m.bytes[len], 0, STORAGE_MAX_VALUE_BYTES - len);

    return send_msg(&m, 0);
}

esp_err_t storage_set_blob_async(const char *ns, const char *key, const void *data, size_t len)
{
    if (len > STORAGE_MAX_VALUE_BYTES)
        return ESP_ERR_INVALID_SIZE;
    if (len > 0 && !data)
        return ESP_ERR_INVALID_ARG;

    storage_msg_t m;
    memset(&m, 0, sizeof(m));
    m.op = OP_SET_BLOB;
    if (!safe_copy_str(m.ns, sizeof(m.ns), ns))
        return ESP_ERR_INVALID_ARG;
    if (!safe_copy_str(m.key, sizeof(m.key), key))
        return ESP_ERR_INVALID_ARG;

    m.len = (uint16_t)len;
    if (len > 0)
        memcpy(m.bytes, data, len);
    if (len < STORAGE_MAX_VALUE_BYTES)
        memset(&m.bytes[len], 0, STORAGE_MAX_VALUE_BYTES - len);

    return send_msg(&m, 0);
}

esp_err_t storage_erase_key_async(const char *ns, const char *key)
{
    storage_msg_t m;
    memset(&m, 0, sizeof(m));
    m.op = OP_ERASE_KEY;
    if (!safe_copy_str(m.ns, sizeof(m.ns), ns))
        return ESP_ERR_INVALID_ARG;
    if (!safe_copy_str(m.key, sizeof(m.key), key))
        return ESP_ERR_INVALID_ARG;
    return send_msg(&m, 0);
}

esp_err_t storage_flush_async(void)
{
    storage_msg_t m;
    memset(&m, 0, sizeof(m));
    m.op = OP_FLUSH;
    return send_msg(&m, 0);
}

// -------------------- sync getters --------------------

esp_err_t storage_get_u8_sync(const char *ns, const char *key, uint8_t *out, TickType_t to)
{
    storage_msg_t req;
    memset(&req, 0, sizeof(req));
    req.op = OP_GET_U8;
    if (!safe_copy_str(req.ns, sizeof(req.ns), ns))
        return ESP_ERR_INVALID_ARG;
    if (!safe_copy_str(req.key, sizeof(req.key), key))
        return ESP_ERR_INVALID_ARG;

    storage_resp_t r;
    esp_err_t err = sync_req(&req, &r, to);
    if (err == ESP_OK && out)
        *out = (uint8_t)(r.value & 0xFF);
    return err;
}

esp_err_t storage_get_u16_sync(const char *ns, const char *key, uint16_t *out, TickType_t to)
{
    storage_msg_t req;
    memset(&req, 0, sizeof(req));
    req.op = OP_GET_U16;
    if (!safe_copy_str(req.ns, sizeof(req.ns), ns))
        return ESP_ERR_INVALID_ARG;
    if (!safe_copy_str(req.key, sizeof(req.key), key))
        return ESP_ERR_INVALID_ARG;

    storage_resp_t r;
    esp_err_t err = sync_req(&req, &r, to);
    if (err == ESP_OK && out)
        *out = (uint16_t)(r.value & 0xFFFF);
    return err;
}

esp_err_t storage_get_u32_sync(const char *ns, const char *key, uint32_t *out, TickType_t to)
{
    storage_msg_t req;
    memset(&req, 0, sizeof(req));
    req.op = OP_GET_U32;
    if (!safe_copy_str(req.ns, sizeof(req.ns), ns))
        return ESP_ERR_INVALID_ARG;
    if (!safe_copy_str(req.key, sizeof(req.key), key))
        return ESP_ERR_INVALID_ARG;

    storage_resp_t r;
    esp_err_t err = sync_req(&req, &r, to);
    if (err == ESP_OK && out)
        *out = r.value;
    return err;
}

esp_err_t storage_get_str_sync(const char *ns, const char *key, char *out, size_t out_sz, TickType_t to)
{
    if (!out || out_sz == 0)
        return ESP_ERR_INVALID_ARG;

    out[0] = '\0';

    storage_msg_t req;
    memset(&req, 0, sizeof(req));
    req.op = OP_GET_STR;
    if (!safe_copy_str(req.ns, sizeof(req.ns), ns))
        return ESP_ERR_INVALID_ARG;
    if (!safe_copy_str(req.key, sizeof(req.key), key))
        return ESP_ERR_INVALID_ARG;

    storage_resp_t r;
    esp_err_t err = sync_req(&req, &r, to);

    if (err != ESP_OK)
    {
        out[out_sz - 1] = '\0';
        return err;
    }

    if (r.len >= out_sz)
    {
        out[0] = '\0';
        out[out_sz - 1] = '\0';
        return ESP_ERR_NVS_INVALID_LENGTH;
    }

    if (r.len > 0)
        memcpy(out, r.bytes, r.len);
    out[r.len] = '\0';
    return ESP_OK;
}

esp_err_t storage_get_blob_sync(const char *ns, const char *key, void *out, size_t *inout_len, TickType_t to)
{
    if (!inout_len)
        return ESP_ERR_INVALID_ARG;

    storage_msg_t req;
    memset(&req, 0, sizeof(req));
    req.op = OP_GET_BLOB;
    req.blob_query_only = (out == NULL);

    if (!safe_copy_str(req.ns, sizeof(req.ns), ns))
        return ESP_ERR_INVALID_ARG;
    if (!safe_copy_str(req.key, sizeof(req.key), key))
        return ESP_ERR_INVALID_ARG;

    storage_resp_t r;
    esp_err_t err = sync_req(&req, &r, to);

    if (err != ESP_OK)
    {
        if (out == NULL && err == ESP_ERR_NOT_FOUND)
            *inout_len = 0;
        return err;
    }

    // query mode: só devolver tamanho requerido
    if (out == NULL)
    {
        *inout_len = r.len;
        return ESP_OK;
    }

    // read mode: valida tamanhos
    if (r.len > *inout_len)
    {
        *inout_len = r.len;
        return ESP_ERR_NVS_INVALID_LENGTH;
    }
    if (r.len > STORAGE_MAX_VALUE_BYTES)
    {
        *inout_len = r.len;
        return ESP_ERR_NVS_INVALID_LENGTH;
    }

    if (r.len > 0)
        memcpy(out, r.bytes, r.len);
    *inout_len = r.len;
    return ESP_OK;
}

esp_err_t storage_flush_sync(TickType_t to)
{
    storage_msg_t req;
    memset(&req, 0, sizeof(req));
    req.op = OP_FLUSH;
    storage_resp_t r;
    return sync_req(&req, &r, to);
}

// -------------------- (upgrade opcional) setters sync --------------------

static esp_err_t set_sync_num(storage_op_t op, const char *ns, const char *key, uint32_t v, TickType_t to)
{
    storage_msg_t req;
    memset(&req, 0, sizeof(req));
    req.op = op;
    if (!safe_copy_str(req.ns, sizeof(req.ns), ns))
        return ESP_ERR_INVALID_ARG;
    if (!safe_copy_str(req.key, sizeof(req.key), key))
        return ESP_ERR_INVALID_ARG;
    req.value = v;

    storage_resp_t r;
    return sync_req(&req, &r, to);
}

esp_err_t storage_set_u8_sync(const char *ns, const char *key, uint8_t v, TickType_t to)
{
    return set_sync_num(OP_SET_U8, ns, key, v, to);
}

esp_err_t storage_set_u16_sync(const char *ns, const char *key, uint16_t v, TickType_t to)
{
    return set_sync_num(OP_SET_U16, ns, key, v, to);
}

esp_err_t storage_set_u32_sync(const char *ns, const char *key, uint32_t v, TickType_t to)
{
    return set_sync_num(OP_SET_U32, ns, key, v, to);
}

esp_err_t storage_set_str_sync(const char *ns, const char *key, const char *str, TickType_t to)
{
    if (!str)
        return ESP_ERR_INVALID_ARG;

    size_t len = strnlen(str, STORAGE_MAX_VALUE_BYTES + 1);
    if (len > STORAGE_MAX_VALUE_BYTES)
        return ESP_ERR_INVALID_SIZE;

    storage_msg_t req;
    memset(&req, 0, sizeof(req));
    req.op = OP_SET_STR;
    if (!safe_copy_str(req.ns, sizeof(req.ns), ns))
        return ESP_ERR_INVALID_ARG;
    if (!safe_copy_str(req.key, sizeof(req.key), key))
        return ESP_ERR_INVALID_ARG;

    req.len = (uint16_t)len;
    if (len > 0)
        memcpy(req.bytes, str, len);

    storage_resp_t r;
    return sync_req(&req, &r, to);
}

esp_err_t storage_set_blob_sync(const char *ns, const char *key, const void *data, size_t len, TickType_t to)
{
    if (len > STORAGE_MAX_VALUE_BYTES)
        return ESP_ERR_INVALID_SIZE;
    if (len > 0 && !data)
        return ESP_ERR_INVALID_ARG;

    storage_msg_t req;
    memset(&req, 0, sizeof(req));
    req.op = OP_SET_BLOB;
    if (!safe_copy_str(req.ns, sizeof(req.ns), ns))
        return ESP_ERR_INVALID_ARG;
    if (!safe_copy_str(req.key, sizeof(req.key), key))
        return ESP_ERR_INVALID_ARG;

    req.len = (uint16_t)len;
    if (len > 0)
        memcpy(req.bytes, data, len);

    storage_resp_t r;
    return sync_req(&req, &r, to);
}

esp_err_t storage_erase_key_sync(const char *ns, const char *key, TickType_t to)
{
    storage_msg_t req;
    memset(&req, 0, sizeof(req));
    req.op = OP_ERASE_KEY;
    if (!safe_copy_str(req.ns, sizeof(req.ns), ns))
        return ESP_ERR_INVALID_ARG;
    if (!safe_copy_str(req.key, sizeof(req.key), key))
        return ESP_ERR_INVALID_ARG;

    storage_resp_t r;
    return sync_req(&req, &r, to);
}
