#include "mqtt.h"

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

#include "esp_log.h"
#include "esp_err.h"
#include "esp_system.h"
#include "esp_event.h"
#include "esp_timer.h"
#include "esp_wifi.h"
#include "esp_netif.h"
#include "esp_mac.h"

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

#include "mqtt_client.h"
#include "cJSON.h"

#include "storage_service.h"

#include "network_events.h"

#include "auth.h"
#include "auth_types.h"
#include "auth_events.h"

#include "scheduler.h"
#include "scheduler_types.h"
#include "scheduler_events.h"

#include "loadbalancer.h"
#include "loadbalancer_events.h"

#include "meter_manager.h"
#include "meter_events.h"

#include "evse_api.h"
#include "evse_config.h"
#include "evse_limits.h"
#include "evse_error.h"
#include "evse_events.h"
#include "evse_session.h"
#include "evse_meter.h"

static const char *TAG = "mqtt";

#define MQTT_NVS_NAMESPACE "mqttcfg"
#define MQTT_KEY_ENABLED "enabled"
#define MQTT_KEY_HOST "host"
#define MQTT_KEY_TOPIC "topic"
#define MQTT_KEY_USER "user"
#define MQTT_KEY_PASS "pass"
#define MQTT_KEY_PERIOD "period"
#define MQTT_KEY_LIMITMODE "limitmode"

#define MQTT_HOST_MAX_LEN 64
#define MQTT_TOPIC_MAX_LEN 64
#define MQTT_USER_MAX_LEN 32
#define MQTT_PASS_MAX_LEN 64

#define MQTT_TOPIC_BUF_LEN 128
#define MQTT_CLIENT_ID_MAX_LEN 32
#define FW_VERSION "1.0"

static bool s_mqtt_enabled = false;
static char s_mqtt_host[MQTT_HOST_MAX_LEN] = {0};
static char s_mqtt_base_topic[MQTT_TOPIC_MAX_LEN] = "evse";
static char s_mqtt_user[MQTT_USER_MAX_LEN] = {0};
static char s_mqtt_pass[MQTT_PASS_MAX_LEN] = {0};
static uint16_t s_mqtt_periodicity = 30;

static limit_mode_t s_limit_mode = LIMIT_MODE_OFF;
static char s_client_id[MQTT_CLIENT_ID_MAX_LEN] = {0};
static bool s_session_was_active = false;

static esp_mqtt_client_handle_t s_client = NULL;
static bool s_connected = false;
static TaskHandle_t s_telemetry_task = NULL;

static inline TickType_t TO_TICKS_MS(uint32_t ms) { return pdMS_TO_TICKS(ms); }

static void mqtt_auth_publish_state(void);
static void mqtt_scheduler_publish_state(void);
static void mqtt_loadbalancer_publish_state(void);
static void mqtt_meters_publish_config(void);
static void mqtt_meters_publish_live(const meter_event_data_t *m, int64_t ts_us);
static void mqtt_evse_publish_state(void);
static void mqtt_limit_publish_state(void);
static void mqtt_session_publish_state(void);
static void mqtt_system_publish_heartbeat(void);

static void mqtt_apply_limit_mode(limit_mode_t mode);
static const char *limit_mode_to_str(limit_mode_t m);
static bool limit_mode_from_str(const char *s, limit_mode_t *out);

static void mqtt_subscribe_all(void);
static void mqtt_dispatch_command(const char *topic, const char *data);

static void mqtt_auth_event_handler(void *arg, esp_event_base_t base, int32_t id, void *data);
static void mqtt_scheduler_event_handler(void *arg, esp_event_base_t base, int32_t id, void *data);
static void mqtt_loadbalancer_event_handler(void *arg, esp_event_base_t base, int32_t id, void *data);
static void mqtt_meter_event_handler(void *arg, esp_event_base_t base, int32_t id, void *ev_data);
static void mqtt_evse_event_handler(void *arg, esp_event_base_t base, int32_t id, void *data);
static void mqtt_network_event_handler(void *arg, esp_event_base_t base, int32_t id, void *data);

static void mqtt_load_config_from_storage(void)
{
    esp_err_t err = storage_service_init();
    if (err != ESP_OK)
    {
        ESP_LOGE(TAG, "storage_service_init failed: %s", esp_err_to_name(err));
        return;
    }

    bool needs_flush = false;

    {
        uint8_t u8 = 0;
        err = storage_get_u8_sync(MQTT_NVS_NAMESPACE, MQTT_KEY_ENABLED, &u8, TO_TICKS_MS(500));
        if (err == ESP_OK && u8 <= 1)
            s_mqtt_enabled = (u8 != 0);
        else
        {
            s_mqtt_enabled = false;
            (void)storage_set_u8_async(MQTT_NVS_NAMESPACE, MQTT_KEY_ENABLED, 0);
            needs_flush = true;
        }
    }

    {
        char tmp[MQTT_HOST_MAX_LEN] = {0};
        err = storage_get_str_sync(MQTT_NVS_NAMESPACE, MQTT_KEY_HOST, tmp, sizeof(tmp), TO_TICKS_MS(500));
        if (err == ESP_OK)
        {
            strncpy(s_mqtt_host, tmp, sizeof(s_mqtt_host));
            s_mqtt_host[sizeof(s_mqtt_host) - 1] = '\0';
        }
        else
        {
            s_mqtt_host[0] = '\0';
            (void)storage_set_str_async(MQTT_NVS_NAMESPACE, MQTT_KEY_HOST, "");
            needs_flush = true;
        }
    }

    {
        char tmp[MQTT_TOPIC_MAX_LEN] = {0};
        err = storage_get_str_sync(MQTT_NVS_NAMESPACE, MQTT_KEY_TOPIC, tmp, sizeof(tmp), TO_TICKS_MS(500));
        if (err == ESP_OK && tmp[0] != '\0')
        {
            strncpy(s_mqtt_base_topic, tmp, sizeof(s_mqtt_base_topic));
            s_mqtt_base_topic[sizeof(s_mqtt_base_topic) - 1] = '\0';
        }
        else
        {
            strncpy(s_mqtt_base_topic, "evse", sizeof(s_mqtt_base_topic));
            s_mqtt_base_topic[sizeof(s_mqtt_base_topic) - 1] = '\0';
            (void)storage_set_str_async(MQTT_NVS_NAMESPACE, MQTT_KEY_TOPIC, s_mqtt_base_topic);
            needs_flush = true;
        }
    }

    {
        char tmp[MQTT_USER_MAX_LEN] = {0};
        err = storage_get_str_sync(MQTT_NVS_NAMESPACE, MQTT_KEY_USER, tmp, sizeof(tmp), TO_TICKS_MS(500));
        if (err == ESP_OK)
        {
            strncpy(s_mqtt_user, tmp, sizeof(s_mqtt_user));
            s_mqtt_user[sizeof(s_mqtt_user) - 1] = '\0';
        }
        else
        {
            s_mqtt_user[0] = '\0';
            (void)storage_set_str_async(MQTT_NVS_NAMESPACE, MQTT_KEY_USER, "");
            needs_flush = true;
        }
    }

    {
        char tmp[MQTT_PASS_MAX_LEN] = {0};
        err = storage_get_str_sync(MQTT_NVS_NAMESPACE, MQTT_KEY_PASS, tmp, sizeof(tmp), TO_TICKS_MS(500));
        if (err == ESP_OK)
        {
            strncpy(s_mqtt_pass, tmp, sizeof(s_mqtt_pass));
            s_mqtt_pass[sizeof(s_mqtt_pass) - 1] = '\0';
        }
        else
        {
            s_mqtt_pass[0] = '\0';
            (void)storage_set_str_async(MQTT_NVS_NAMESPACE, MQTT_KEY_PASS, "");
            needs_flush = true;
        }
    }

    {
        uint16_t u16 = 30;
        err = storage_get_u16_sync(MQTT_NVS_NAMESPACE, MQTT_KEY_PERIOD, &u16, TO_TICKS_MS(500));
        if (err == ESP_OK && u16 > 0 && u16 < 3600)
            s_mqtt_periodicity = u16;
        else
        {
            s_mqtt_periodicity = 30;
            (void)storage_set_u16_async(MQTT_NVS_NAMESPACE, MQTT_KEY_PERIOD, s_mqtt_periodicity);
            needs_flush = true;
        }
    }

    {
        uint8_t u8 = 0;
        err = storage_get_u8_sync(MQTT_NVS_NAMESPACE, MQTT_KEY_LIMITMODE, &u8, TO_TICKS_MS(500));
        if (err == ESP_OK && u8 <= LIMIT_MODE_SOLAR)
            s_limit_mode = (limit_mode_t)u8;
        else
        {
            s_limit_mode = LIMIT_MODE_OFF;
            (void)storage_set_u8_async(MQTT_NVS_NAMESPACE, MQTT_KEY_LIMITMODE, 0);
            needs_flush = true;
        }
    }

    if (needs_flush)
        (void)storage_flush_async();

    ESP_LOGI(TAG, "Config MQTT (storage): enabled=%d host='%s' topic='%s' period=%u user='%s' limitmode=%s",
             s_mqtt_enabled, s_mqtt_host, s_mqtt_base_topic,
             (unsigned)s_mqtt_periodicity, s_mqtt_user, limit_mode_to_str(s_limit_mode));
}

static esp_err_t mqtt_save_config_to_storage(void)
{
    esp_err_t err = storage_service_init();
    if (err != ESP_OK)
        return err;

    esp_err_t e = ESP_OK;

    e = storage_set_u8_async(MQTT_NVS_NAMESPACE, MQTT_KEY_ENABLED, s_mqtt_enabled ? 1 : 0);
    if (e != ESP_OK)
        err = e;

    e = storage_set_str_async(MQTT_NVS_NAMESPACE, MQTT_KEY_HOST, s_mqtt_host);
    if (e != ESP_OK)
        err = e;

    e = storage_set_str_async(MQTT_NVS_NAMESPACE, MQTT_KEY_TOPIC, s_mqtt_base_topic);
    if (e != ESP_OK)
        err = e;

    e = storage_set_str_async(MQTT_NVS_NAMESPACE, MQTT_KEY_USER, s_mqtt_user);
    if (e != ESP_OK)
        err = e;

    e = storage_set_str_async(MQTT_NVS_NAMESPACE, MQTT_KEY_PASS, s_mqtt_pass);
    if (e != ESP_OK)
        err = e;

    e = storage_set_u16_async(MQTT_NVS_NAMESPACE, MQTT_KEY_PERIOD, s_mqtt_periodicity);
    if (e != ESP_OK)
        err = e;

    e = storage_set_u8_async(MQTT_NVS_NAMESPACE, MQTT_KEY_LIMITMODE, (uint8_t)s_limit_mode);
    if (e != ESP_OK)
        err = e;

    (void)storage_flush_async();

    if (err != ESP_OK)
        ESP_LOGE(TAG, "mqtt_save_config_to_storage had errors: %s", esp_err_to_name(err));

    return err;
}

bool mqtt_get_enabled(void) { return s_mqtt_enabled; }

void mqtt_get_server(char *server)
{
    if (!server)
        return;
    strncpy(server, s_mqtt_host, MQTT_HOST_MAX_LEN);
    server[MQTT_HOST_MAX_LEN - 1] = '\0';
}

void mqtt_get_base_topic(char *base_topic)
{
    if (!base_topic)
        return;
    strncpy(base_topic, s_mqtt_base_topic, MQTT_TOPIC_MAX_LEN);
    base_topic[MQTT_TOPIC_MAX_LEN - 1] = '\0';
}

void mqtt_get_user(char *username)
{
    if (!username)
        return;
    strncpy(username, s_mqtt_user, MQTT_USER_MAX_LEN);
    username[MQTT_USER_MAX_LEN - 1] = '\0';
}

void mqtt_get_password(char *password)
{
    if (!password)
        return;
    strncpy(password, s_mqtt_pass, MQTT_PASS_MAX_LEN);
    password[MQTT_PASS_MAX_LEN - 1] = '\0';
}

uint16_t mqtt_get_periodicity(void) { return s_mqtt_periodicity; }

static const char *limit_mode_to_str(limit_mode_t m)
{
    switch (m)
    {
    case LIMIT_MODE_OFF:
        return "off";
    case LIMIT_MODE_MANUAL:
        return "manual";
    case LIMIT_MODE_LOADBALANCER:
        return "loadbalancer";
    case LIMIT_MODE_SOLAR:
        return "solar";
    default:
        return "off";
    }
}

static bool limit_mode_from_str(const char *s, limit_mode_t *out)
{
    if (!s || !out)
        return false;
    if (strcmp(s, "off") == 0)
    {
        *out = LIMIT_MODE_OFF;
        return true;
    }
    if (strcmp(s, "manual") == 0)
    {
        *out = LIMIT_MODE_MANUAL;
        return true;
    }
    if (strcmp(s, "loadbalancer") == 0)
    {
        *out = LIMIT_MODE_LOADBALANCER;
        return true;
    }
    if (strcmp(s, "solar") == 0)
    {
        *out = LIMIT_MODE_SOLAR;
        return true;
    }
    return false;
}

static void mqtt_apply_limit_mode(limit_mode_t mode)
{
    switch (mode)
    {
    case LIMIT_MODE_OFF:
        loadbalancer_pv_set_enabled(false);
        loadbalancer_grid_set_enabled(false);
        loadbalancer_set_enabled(false);
        evse_set_charging_current(evse_get_max_charging_current());
        break;

    case LIMIT_MODE_MANUAL:
        loadbalancer_pv_set_enabled(false);
        loadbalancer_grid_set_enabled(false);
        loadbalancer_set_enabled(false);
        break;

    case LIMIT_MODE_LOADBALANCER:
        loadbalancer_pv_set_enabled(false);
        loadbalancer_grid_set_enabled(true);
        loadbalancer_set_enabled(true);
        break;

    case LIMIT_MODE_SOLAR:
        loadbalancer_grid_set_enabled(false);
        loadbalancer_pv_set_enabled(true);
        loadbalancer_set_enabled(true);
        break;
    }

    s_limit_mode = mode;
    (void)storage_set_u8_async(MQTT_NVS_NAMESPACE, MQTT_KEY_LIMITMODE, (uint8_t)mode);
    (void)storage_flush_async();
}

static void mqtt_build_topic(char *out, size_t out_len, const char *suffix)
{
    const char *base = (s_mqtt_base_topic[0] != '\0') ? s_mqtt_base_topic : "evse";
    if (suffix && suffix[0] != '\0')
        snprintf(out, out_len, "%s/%s", base, suffix);
    else
        snprintf(out, out_len, "%s", base);
}

static void mqtt_publish_raw(const char *subtopic, const char *payload, bool retain)
{
    if (!s_client || !s_connected || !s_mqtt_enabled || !payload)
        return;

    char topic[MQTT_TOPIC_BUF_LEN];
    mqtt_build_topic(topic, sizeof(topic), subtopic);

    int msg_id = esp_mqtt_client_publish(s_client, topic, payload, 0, 1, retain ? 1 : 0);
    ESP_LOGD(TAG, "MQTT publish [%s] (id=%d): %s", topic, msg_id, payload);
}

static void mqtt_publish_json(const char *subtopic, cJSON *obj, bool retain)
{
    if (!obj)
        return;

    char *str = cJSON_PrintUnformatted(obj);
    if (!str)
    {
        cJSON_Delete(obj);
        return;
    }

    mqtt_publish_raw(subtopic, str, retain);

    free(str);
    cJSON_Delete(obj);
}

static bool parse_hhmm(const char *s, uint16_t *out_min)
{
    if (!s || !out_min)
        return false;

    int h = 0, m = 0;
    if (sscanf(s, "%d:%d", &h, &m) != 2)
        return false;

    if (h == 24 && m == 0)
    {
        *out_min = 1440;
        return true;
    }

    if (h < 0 || h > 23 || m < 0 || m > 59)
        return false;

    *out_min = (uint16_t)(h * 60 + m);
    return true;
}

static void format_hhmm(uint16_t minutes, char *buf, size_t buf_sz)
{
    if (!buf || buf_sz < 6)
        return;

    if (minutes == 1440)
    {
        snprintf(buf, buf_sz, "24:00");
        return;
    }

    minutes %= 1440;
    snprintf(buf, buf_sz, "%02d:%02d", minutes / 60, minutes % 60);
}

static void mqtt_auth_publish_state(void)
{
    cJSON *root = cJSON_CreateObject();
    if (!root)
        return;

    auth_mode_t mode = auth_get_mode();
    cJSON_AddStringToObject(root, "mode", auth_mode_to_str(mode));

    int count = auth_get_tag_count();
    cJSON *tags = cJSON_CreateArray();
    for (int i = 0; i < count; ++i)
    {
        const char *tag = auth_get_tag_by_index(i);
        if (tag)
            cJSON_AddItemToArray(tags, cJSON_CreateString(tag));
    }
    cJSON_AddItemToObject(root, "tags", tags);

    mqtt_publish_json("state/auth", root, true);
}

static cJSON *sched_window_to_json(const sched_window_t *w)
{
    if (!w)
        return NULL;

    cJSON *o = cJSON_CreateObject();
    if (!o)
        return NULL;

    char buf[8];
    format_hhmm(w->start_min, buf, sizeof(buf));
    cJSON_AddStringToObject(o, "startTime", buf);
    format_hhmm(w->end_min, buf, sizeof(buf));
    cJSON_AddStringToObject(o, "endTime", buf);
    cJSON_AddNumberToObject(o, "currentA", (int)w->current_a);

    return o;
}

static bool sched_parse_windows_array(cJSON *arr, sched_config_t *cfg)
{
    if (!arr || !cfg || !cJSON_IsArray(arr))
        return false;

    int n = cJSON_GetArraySize(arr);
    if (n <= 0)
        return false;
    if (n > SCHED_MAX_WINDOWS)
        n = SCHED_MAX_WINDOWS;

    cfg->window_count = 0;

    for (int i = 0; i < n; i++)
    {
        cJSON *it = cJSON_GetArrayItem(arr, i);
        if (!cJSON_IsObject(it))
            return false;

        cJSON *js = cJSON_GetObjectItem(it, "startTime");
        cJSON *je = cJSON_GetObjectItem(it, "endTime");
        cJSON *ja = cJSON_GetObjectItem(it, "currentA");

        if (!cJSON_IsString(js) || !cJSON_IsString(je) || !cJSON_IsNumber(ja))
            return false;

        uint16_t sm = 0, em = 0;
        if (!parse_hhmm(js->valuestring, &sm))
            return false;
        if (!parse_hhmm(je->valuestring, &em))
            return false;

        int a = ja->valueint;
        if (a < 6 || a > 80)
            return false;

        cfg->windows[cfg->window_count].start_min = sm;
        cfg->windows[cfg->window_count].end_min = em;
        cfg->windows[cfg->window_count].current_a = (uint16_t)a;
        cfg->window_count++;
    }

    return true;
}

static void mqtt_scheduler_publish_state(void)
{
    sched_config_t cfg = scheduler_get_config();

    cJSON *root = cJSON_CreateObject();
    if (!root)
        return;

    cJSON_AddBoolToObject(root, "enabled", cfg.enabled);
    cJSON_AddStringToObject(root, "mode", sched_mode_to_str(cfg.mode));

    cJSON *arr = cJSON_CreateArray();
    if (arr)
    {
        for (int i = 0; i < cfg.window_count && i < SCHED_MAX_WINDOWS; i++)
        {
            cJSON *w = sched_window_to_json(&cfg.windows[i]);
            if (w)
                cJSON_AddItemToArray(arr, w);
        }
        cJSON_AddItemToObject(root, "windows", arr);
    }

    cJSON_AddBoolToObject(root, "allowedNow", scheduler_is_allowed_now());
    cJSON_AddNumberToObject(root, "currentLimitNowA", (int)scheduler_get_current_limit_now_a());

    mqtt_publish_json("state/scheduler", root, true);
}

static void mqtt_loadbalancer_publish_state(void)
{
    cJSON *root = cJSON_CreateObject();
    if (!root)
        return;

    cJSON_AddBoolToObject(root, "enabled", loadbalancer_is_enabled());
    cJSON_AddNumberToObject(root, "currentLimitA", load_balancing_get_max_grid_current());

    mqtt_publish_json("state/loadbalancing", root, true);
}

static void mqtt_meters_publish_config(void)
{
    cJSON *root = cJSON_CreateObject();
    if (!root)
        return;

    meter_type_t grid = meter_manager_grid_get_model();
    meter_type_t evse = meter_manager_evse_get_model();

    cJSON_AddStringToObject(root, "gridmeter", meter_type_to_str(grid));
    cJSON_AddStringToObject(root, "evsemeter", meter_type_to_str(evse));

    mqtt_publish_json("state/meters-config", root, true);
}

static void mqtt_meters_publish_live(const meter_event_data_t *m, int64_t ts_us)
{
    if (!m)
        return;

    cJSON *root = cJSON_CreateObject();
    if (!root)
        return;

    cJSON_AddStringToObject(root, "source", m->source ? m->source : "GRID");
    cJSON_AddNumberToObject(root, "frequency", m->frequency);
    cJSON_AddNumberToObject(root, "powerFactor", m->power_factor);
    cJSON_AddNumberToObject(root, "meterTotalEnergyKWh", m->total_energy);
    cJSON_AddNumberToObject(root, "timestampUs", (double)ts_us);

    cJSON *vr = cJSON_CreateArray();
    cJSON *ir = cJSON_CreateArray();
    cJSON *pw = cJSON_CreateArray();
    for (int i = 0; i < 3; ++i)
    {
        cJSON_AddItemToArray(vr, cJSON_CreateNumber(m->vrms[i]));
        cJSON_AddItemToArray(ir, cJSON_CreateNumber(m->irms[i]));
        cJSON_AddItemToArray(pw, cJSON_CreateNumber(m->watt[i]));
    }
    cJSON_AddItemToObject(root, "vrms", vr);
    cJSON_AddItemToObject(root, "irms", ir);
    cJSON_AddItemToObject(root, "watt", pw);

    const char *suffix = (m->source && strcmp(m->source, "EVSE") == 0)
                             ? "state/meter/evse"
                             : "state/meter/grid";

    mqtt_publish_json(suffix, root, false);
}

static void mqtt_evse_publish_state(void)
{
    evse_state_t state = evse_get_state();
    evse_session_t s = {0};

    cJSON *dash = cJSON_CreateObject();
    if (!dash)
        return;

    cJSON_AddStringToObject(dash, "status", evse_state_to_str(state));
    cJSON_AddNumberToObject(dash, "powerW", evse_meter_get_instant_power());
    cJSON_AddNumberToObject(dash, "currentA", evse_get_runtime_charging_current());
    cJSON_AddNumberToObject(dash, "maxCurrentA", evse_get_charging_current());

    if (evse_session_get(&s))
    {
        cJSON_AddNumberToObject(dash, "sessionId", (double)s.session_id);
        if (s.tag[0] != '\0')
            cJSON_AddStringToObject(dash, "sessionTag", s.tag);
        cJSON_AddNumberToObject(dash, "sessionEnergyWh", (double)s.energy_wh);
        cJSON_AddNumberToObject(dash, "sessionDurationS", (double)s.duration_s);
    }
    else
    {
        cJSON_AddNumberToObject(dash, "sessionId", 0);
        cJSON_AddNumberToObject(dash, "sessionEnergyWh", 0);
        cJSON_AddNumberToObject(dash, "sessionDurationS", 0);
    }

    cJSON *alerts = cJSON_CreateArray();
    if (alerts)
    {
        if (evse_is_limit_reached())
            cJSON_AddItemToArray(alerts, cJSON_CreateString("Limite de consumo atingido."));
        if (!evse_config_is_available())
            cJSON_AddItemToArray(alerts, cJSON_CreateString("Estação indisponível."));
        if (!evse_config_is_enabled())
            cJSON_AddItemToArray(alerts, cJSON_CreateString("EVSE desativado."));
        cJSON_AddItemToObject(dash, "alerts", alerts);
    }

    uint32_t error_bits = evse_get_error();
    cJSON *errors = cJSON_CreateArray();
    if (errors)
    {
        if (error_bits & EVSE_ERR_DIODE_SHORT_BIT)
            cJSON_AddItemToArray(errors, cJSON_CreateString("Diodo curto-circuitado"));
        if (error_bits & EVSE_ERR_LOCK_FAULT_BIT)
            cJSON_AddItemToArray(errors, cJSON_CreateString("Falha no travamento"));
        if (error_bits & EVSE_ERR_UNLOCK_FAULT_BIT)
            cJSON_AddItemToArray(errors, cJSON_CreateString("Falha no destravamento"));
        if (error_bits & EVSE_ERR_RCM_SELFTEST_FAULT_BIT)
            cJSON_AddItemToArray(errors, cJSON_CreateString("Falha no autoteste do RCM"));
        if (error_bits & EVSE_ERR_RCM_TRIGGERED_BIT)
            cJSON_AddItemToArray(errors, cJSON_CreateString("RCM disparado"));
        if (error_bits & EVSE_ERR_TEMPERATURE_HIGH_BIT)
            cJSON_AddItemToArray(errors, cJSON_CreateString("Temperatura elevada"));
        if (error_bits & EVSE_ERR_PILOT_FAULT_BIT)
            cJSON_AddItemToArray(errors, cJSON_CreateString("Erro no sinal piloto"));
        if (error_bits & EVSE_ERR_TEMPERATURE_FAULT_BIT)
            cJSON_AddItemToArray(errors, cJSON_CreateString("Falha no sensor de temperatura"));
        cJSON_AddItemToObject(dash, "errors", errors);
    }

    mqtt_publish_json("state/evse", dash, true);
}

static void mqtt_limit_publish_state(void)
{
    cJSON *root = cJSON_CreateObject();
    if (!root)
        return;

    cJSON_AddStringToObject(root, "mode", limit_mode_to_str(s_limit_mode));
    cJSON_AddNumberToObject(root, "requestedCurrentA", (int)evse_get_charging_current());
    cJSON_AddNumberToObject(root, "effectiveCurrentA", (int)evse_get_runtime_charging_current());
    cJSON_AddNumberToObject(root, "hardwareMaxA", (int)evse_get_max_charging_current());
    cJSON_AddNumberToObject(root, "gridMaxImportA", (int)loadbalancer_grid_get_max_import_a());
    cJSON_AddNumberToObject(root, "maxImportW", (int)loadbalancer_pv_get_max_import_w());

    mqtt_publish_json("state/limit", root, true);
}

static void mqtt_session_publish_state(void)
{
    evse_session_t s = {0};
    if (!evse_session_get(&s))
        return;

    cJSON *root = cJSON_CreateObject();
    if (!root)
        return;

    cJSON_AddNumberToObject(root, "sessionId", (double)s.session_id);
    if (s.tag[0] != '\0')
        cJSON_AddStringToObject(root, "tag", s.tag);
    cJSON_AddBoolToObject(root, "isCurrent", s.is_current);
    cJSON_AddNumberToObject(root, "durationS", (double)s.duration_s);
    cJSON_AddNumberToObject(root, "energyWh", (double)s.energy_wh);
    cJSON_AddNumberToObject(root, "avgPowerW", (double)s.avg_power_w);

    mqtt_publish_json("state/session", root, !s.is_current);
}

static void mqtt_system_publish_heartbeat(void)
{
    cJSON *root = cJSON_CreateObject();
    if (!root)
        return;

    int64_t uptime_us = esp_timer_get_time();
    cJSON_AddNumberToObject(root, "uptimeS", (double)(uptime_us / 1000000));
    cJSON_AddNumberToObject(root, "freeHeap", (double)esp_get_free_heap_size());
    cJSON_AddStringToObject(root, "fwVersion", FW_VERSION);

    esp_netif_t *netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
    if (netif)
    {
        esp_netif_ip_info_t ip_info;
        if (esp_netif_get_ip_info(netif, &ip_info) == ESP_OK)
        {
            char ip_str[16];
            snprintf(ip_str, sizeof(ip_str), IPSTR, IP2STR(&ip_info.ip));
            cJSON_AddStringToObject(root, "ip", ip_str);
        }
    }

    wifi_ap_record_t ap;
    if (esp_wifi_sta_get_ap_info(&ap) == ESP_OK)
        cJSON_AddNumberToObject(root, "rssi", ap.rssi);

    mqtt_publish_json("state/system", root, true);
}

static void mqtt_auth_event_handler(void *arg, esp_event_base_t base, int32_t id, void *data)
{
    (void)arg;
    (void)data;
    if (base != AUTH_EVENTS)
        return;

    switch (id)
    {
    case AUTH_EVENT_INIT:
    case AUTH_EVENT_MODE_CHANGED:
    case AUTH_EVENT_TAG_SAVED:
        mqtt_auth_publish_state();
        break;
    default:
        break;
    }
}

static void mqtt_scheduler_event_handler(void *arg, esp_event_base_t base, int32_t id, void *data)
{
    (void)arg;
    (void)data;
    if (base != SCHED_EVENTS)
        return;

    switch (id)
    {
    case SCHED_EVENT_INIT:
    case SCHED_EVENT_WINDOW_CHANGED:
        mqtt_scheduler_publish_state();
        break;
    default:
        break;
    }
}

static void mqtt_loadbalancer_event_handler(void *arg, esp_event_base_t base, int32_t id, void *data)
{
    (void)arg;
    (void)data;
    if (base != LOADBALANCER_EVENTS)
        return;

    switch (id)
    {
    case LOADBALANCER_EVENT_INIT:
    case LOADBALANCER_EVENT_STATE_CHANGED:
    case LOADBALANCER_EVENT_GLOBAL_CURRENT_LIMIT:
        mqtt_loadbalancer_publish_state();
        mqtt_limit_publish_state();
        break;
    default:
        break;
    }
}

static void mqtt_meter_event_handler(void *arg, esp_event_base_t base, int32_t id, void *ev_data)
{
    (void)arg;
    if (base != METER_EVENT)
        return;

    switch (id)
    {
    case METER_EVENT_DATA_READY:
    {
        const meter_event_data_t *m = (const meter_event_data_t *)ev_data;
        int64_t now = esp_timer_get_time();
        mqtt_meters_publish_live(m, now);
        break;
    }
    case METER_EVENT_CONFIG_UPDATED:
        mqtt_meters_publish_config();
        break;
    default:
        break;
    }
}

static void mqtt_evse_event_handler(void *arg, esp_event_base_t base, int32_t id, void *data)
{
    (void)arg;
    (void)data;
    if (base != EVSE_EVENTS)
        return;

    switch (id)
    {
    case EVSE_EVENT_INIT:
    case EVSE_EVENT_STATE_CHANGED:
    case EVSE_EVENT_CONFIG_UPDATED:
    case EVSE_EVENT_ENABLE_UPDATED:
    case EVSE_EVENT_AVAILABLE_UPDATED:
    case EVSE_EVENT_ERROR_CHANGED:
        mqtt_evse_publish_state();

        {
            evse_session_t s = {0};
            bool has = evse_session_get(&s);
            bool active_now = has && s.is_current;

            if (active_now != s_session_was_active)
            {
                mqtt_session_publish_state();
                s_session_was_active = active_now;
            }
        }
        break;
    default:
        break;
    }
}

static void handle_cmd_auth_mode_set(const char *payload)
{
    cJSON *json = cJSON_Parse(payload);
    if (!json)
    {
        ESP_LOGW(TAG, "Invalid JSON in auth/mode/set");
        return;
    }

    cJSON *j_mode = cJSON_GetObjectItem(json, "mode");
    if (cJSON_IsString(j_mode) && j_mode->valuestring)
    {
        auth_mode_t mode;
        if (auth_mode_from_str(j_mode->valuestring, &mode))
        {
            ESP_LOGI(TAG, "MQTT: setting auth mode = %s", j_mode->valuestring);
            auth_set_mode(mode);
        }
        else
        {
            ESP_LOGW(TAG, "Invalid auth mode: %s", j_mode->valuestring);
        }
    }

    cJSON_Delete(json);
}

static void handle_cmd_auth_tag_add(const char *payload)
{
    cJSON *json = cJSON_Parse(payload);
    if (!json)
    {
        ESP_LOGW(TAG, "Invalid JSON in auth/tags/add");
        return;
    }

    cJSON *j_tag = cJSON_GetObjectItem(json, "tag");
    if (cJSON_IsString(j_tag) && j_tag->valuestring)
    {
        ESP_LOGI(TAG, "MQTT: adding tag '%s'", j_tag->valuestring);
        if (!auth_add_tag(j_tag->valuestring))
            ESP_LOGW(TAG, "auth_add_tag failed");
        mqtt_auth_publish_state();
    }

    cJSON_Delete(json);
}

static void handle_cmd_auth_tag_remove(const char *payload)
{
    cJSON *json = cJSON_Parse(payload);
    if (!json)
    {
        ESP_LOGW(TAG, "Invalid JSON in auth/tags/remove");
        return;
    }

    cJSON *j_tag = cJSON_GetObjectItem(json, "tag");
    if (cJSON_IsString(j_tag) && j_tag->valuestring)
    {
        ESP_LOGI(TAG, "MQTT: removing tag '%s'", j_tag->valuestring);
        if (!auth_remove_tag(j_tag->valuestring))
            ESP_LOGW(TAG, "auth_remove_tag failed");
        mqtt_auth_publish_state();
    }

    cJSON_Delete(json);
}

static void handle_cmd_scheduler_set(const char *payload)
{
    cJSON *json = cJSON_Parse(payload);
    if (!json)
    {
        ESP_LOGW(TAG, "Invalid JSON in scheduler/set");
        return;
    }

    sched_config_t cfg = scheduler_get_config();

    cJSON *j_enabled = cJSON_GetObjectItem(json, "enabled");
    if (cJSON_IsBool(j_enabled))
        cfg.enabled = cJSON_IsTrue(j_enabled);

    if (!cfg.enabled)
    {
        cfg.mode = SCHED_MODE_DISABLED;
        cfg.window_count = 0;
        scheduler_set_config(&cfg);
        cJSON_Delete(json);
        return;
    }

    cfg.mode = SCHED_MODE_DAILY_MULTI;

    cJSON *j_windows = cJSON_GetObjectItem(json, "windows");
    if (!j_windows || !cJSON_IsArray(j_windows))
    {
        ESP_LOGW(TAG, "scheduler/set missing windows[]");
        cJSON_Delete(json);
        return;
    }

    if (!sched_parse_windows_array(j_windows, &cfg))
    {
        ESP_LOGW(TAG, "scheduler/set invalid windows[]");
        cJSON_Delete(json);
        return;
    }

    scheduler_set_config(&cfg);
    cJSON_Delete(json);
}

static void handle_cmd_loadbalancing_set(const char *payload)
{
    cJSON *json = cJSON_Parse(payload);
    if (!json)
    {
        ESP_LOGW(TAG, "Invalid JSON in loadbalancing/set");
        return;
    }

    cJSON *j_enabled = cJSON_GetObjectItem(json, "enabled");
    if (!j_enabled)
        j_enabled = cJSON_GetObjectItem(json, "loadBalancingEnabled");
    if (j_enabled && cJSON_IsBool(j_enabled))
    {
        bool enabled = cJSON_IsTrue(j_enabled);
        ESP_LOGI(TAG, "MQTT: set loadbalancer enabled = %d", enabled);
        loadbalancer_set_enabled(enabled);
    }

    cJSON *j_limit = cJSON_GetObjectItem(json, "currentLimitA");
    if (!j_limit)
        j_limit = cJSON_GetObjectItem(json, "loadBalancingCurrentLimit");
    if (j_limit && cJSON_IsNumber(j_limit))
    {
        int limit = j_limit->valueint;
        if (limit < 6 || limit > 100)
        {
            ESP_LOGW(TAG, "Invalid loadbalancer current limit: %d", limit);
        }
        else
        {
            ESP_LOGI(TAG, "MQTT: set max_grid_current = %d", limit);
            esp_err_t err = load_balancing_set_max_grid_current((uint8_t)limit);
            if (err != ESP_OK)
                ESP_LOGE(TAG, "load_balancing_set_max_grid_current failed: %s", esp_err_to_name(err));
        }
    }

    cJSON_Delete(json);
}

static void handle_cmd_meters_set(const char *payload)
{
    cJSON *json = cJSON_Parse(payload);
    if (!json)
    {
        ESP_LOGW(TAG, "Invalid JSON in meters/set");
        return;
    }

    cJSON *j_grid = cJSON_GetObjectItem(json, "gridmeter");
    if (j_grid && cJSON_IsString(j_grid))
    {
        meter_type_t type = string_to_meter_type(j_grid->valuestring);
        ESP_LOGI(TAG, "MQTT: set gridmeter = %s", j_grid->valuestring);
        meter_manager_grid_set_model(type);
    }

    cJSON *j_evse = cJSON_GetObjectItem(json, "evsemeter");
    if (j_evse && cJSON_IsString(j_evse))
    {
        meter_type_t type = string_to_meter_type(j_evse->valuestring);
        ESP_LOGI(TAG, "MQTT: set evsemeter = %s", j_evse->valuestring);
        meter_manager_evse_set_model(type);
    }

    cJSON_Delete(json);
}

static void handle_cmd_evse_settings(const char *payload)
{
    cJSON *json = cJSON_Parse(payload);
    if (!json)
    {
        ESP_LOGW(TAG, "Invalid JSON in evse/settings");
        return;
    }

    cJSON *j_current = cJSON_GetObjectItem(json, "currentLimit");
    if (j_current && cJSON_IsNumber(j_current))
    {
        int curr = j_current->valueint;
        ESP_LOGI(TAG, "MQTT: set EVSE currentLimit = %d", curr);
        evse_set_charging_current((uint8_t)curr);
    }

    cJSON *j_temp = cJSON_GetObjectItem(json, "temperatureLimit");
    if (j_temp && cJSON_IsNumber(j_temp))
    {
        int t = j_temp->valueint;
        ESP_LOGI(TAG, "MQTT: set EVSE temperatureLimit = %d", t);
        evse_set_temp_threshold((uint8_t)t);
    }

    cJSON_Delete(json);
    mqtt_evse_publish_state();
    mqtt_limit_publish_state();
}

static void handle_cmd_evse_action(const char *payload)
{
    cJSON *json = cJSON_Parse(payload);
    if (!json)
    {
        ESP_LOGW(TAG, "Invalid JSON in evse/action");
        return;
    }

    cJSON *j_action = cJSON_GetObjectItem(json, "action");
    if (!cJSON_IsString(j_action) || !j_action->valuestring)
    {
        ESP_LOGW(TAG, "evse/action: missing 'action'");
        cJSON_Delete(json);
        return;
    }

    const char *act = j_action->valuestring;

    if (strcmp(act, "available") == 0)
    {
        ESP_LOGI(TAG, "MQTT: evse set available=true");
        evse_set_available(true);
    }
    else if (strcmp(act, "unavailable") == 0)
    {
        ESP_LOGI(TAG, "MQTT: evse set available=false");
        evse_set_available(false);
    }
    else
    {
        ESP_LOGW(TAG, "evse/action: unknown action '%s'", act);
    }

    cJSON_Delete(json);
    mqtt_evse_publish_state();
}

static void handle_cmd_evse_limit(const char *payload)
{
    cJSON *json = cJSON_Parse(payload);
    if (!json)
    {
        ESP_LOGW(TAG, "Invalid JSON in evse/limit");
        return;
    }

    cJSON *j_mode = cJSON_GetObjectItem(json, "mode");
    if (!cJSON_IsString(j_mode) || !j_mode->valuestring)
    {
        ESP_LOGW(TAG, "evse/limit: missing 'mode'");
        cJSON_Delete(json);
        return;
    }

    limit_mode_t mode;
    if (!limit_mode_from_str(j_mode->valuestring, &mode))
    {
        ESP_LOGW(TAG, "evse/limit: unknown mode '%s'", j_mode->valuestring);
        cJSON_Delete(json);
        return;
    }

    switch (mode)
    {
    case LIMIT_MODE_MANUAL:
    {
        cJSON *j_curr = cJSON_GetObjectItem(json, "requestedCurrentA");
        if (!j_curr)
            j_curr = cJSON_GetObjectItem(json, "currentA");

        if (!cJSON_IsNumber(j_curr))
        {
            ESP_LOGW(TAG, "evse/limit manual: missing 'requestedCurrentA/currentA'");
            cJSON_Delete(json);
            return;
        }

        int a = j_curr->valueint;
        uint8_t hw_max = evse_get_max_charging_current();
        if (a < MIN_CHARGING_CURRENT_LIMIT || a > hw_max)
        {
            ESP_LOGW(TAG, "evse/limit manual: currentA %d out of range [%d,%u]",
                     a, MIN_CHARGING_CURRENT_LIMIT, (unsigned)hw_max);
            cJSON_Delete(json);
            return;
        }

        evse_set_charging_current((uint16_t)a);
        break;
    }

    case LIMIT_MODE_LOADBALANCER:
    {
        cJSON *j = cJSON_GetObjectItem(json, "gridMaxImportA");
        if (cJSON_IsNumber(j))
        {
            int a = j->valueint;
            if (a < 6 || a > 100)
            {
                ESP_LOGW(TAG, "evse/limit loadbalancer: gridMaxImportA %d out of range", a);
                cJSON_Delete(json);
                return;
            }
            esp_err_t e = loadbalancer_grid_set_max_import_a((uint8_t)a);
            if (e != ESP_OK)
                ESP_LOGW(TAG, "loadbalancer_grid_set_max_import_a failed: %s", esp_err_to_name(e));
        }
        break;
    }

    case LIMIT_MODE_SOLAR:
    {
        cJSON *j = cJSON_GetObjectItem(json, "maxImportW");
        if (cJSON_IsNumber(j))
        {
            int w = j->valueint;
            if (w < 0)
            {
                ESP_LOGW(TAG, "evse/limit solar: maxImportW %d invalid", w);
                cJSON_Delete(json);
                return;
            }
            esp_err_t e = loadbalancer_pv_set_max_import_w(w);
            if (e != ESP_OK)
                ESP_LOGW(TAG, "loadbalancer_pv_set_max_import_w failed: %s", esp_err_to_name(e));
        }
        break;
    }

    case LIMIT_MODE_OFF:
        break;
    }

    ESP_LOGI(TAG, "MQTT: applying limit mode = %s", limit_mode_to_str(mode));
    mqtt_apply_limit_mode(mode);

    cJSON_Delete(json);
    mqtt_loadbalancer_publish_state();
    mqtt_limit_publish_state();
    mqtt_evse_publish_state();
}

static void mqtt_subscribe_all(void)
{
    if (!s_client)
        return;

    char topic[MQTT_TOPIC_BUF_LEN];

    mqtt_build_topic(topic, sizeof(topic), "cmd/auth/mode/set");
    esp_mqtt_client_subscribe(s_client, topic, 1);

    mqtt_build_topic(topic, sizeof(topic), "cmd/auth/tags/add");
    esp_mqtt_client_subscribe(s_client, topic, 1);

    mqtt_build_topic(topic, sizeof(topic), "cmd/auth/tags/remove");
    esp_mqtt_client_subscribe(s_client, topic, 1);

    mqtt_build_topic(topic, sizeof(topic), "cmd/scheduler/set");
    esp_mqtt_client_subscribe(s_client, topic, 1);

    mqtt_build_topic(topic, sizeof(topic), "cmd/loadbalancing/set");
    esp_mqtt_client_subscribe(s_client, topic, 1);

    mqtt_build_topic(topic, sizeof(topic), "cmd/meters/set");
    esp_mqtt_client_subscribe(s_client, topic, 1);

    mqtt_build_topic(topic, sizeof(topic), "cmd/evse/settings");
    esp_mqtt_client_subscribe(s_client, topic, 1);

    mqtt_build_topic(topic, sizeof(topic), "cmd/evse/action");
    esp_mqtt_client_subscribe(s_client, topic, 1);

    mqtt_build_topic(topic, sizeof(topic), "cmd/evse/limit");
    esp_mqtt_client_subscribe(s_client, topic, 1);
}

static void mqtt_dispatch_command(const char *topic, const char *data)
{
    if (!topic || !data)
        return;

    const char *base = (s_mqtt_base_topic[0] != '\0') ? s_mqtt_base_topic : "evse";
    size_t base_len = strlen(base);

    if (strncmp(topic, base, base_len) != 0)
        return;

    const char *sub = topic + base_len;
    if (*sub == '/')
        sub++;

    if (strcmp(sub, "cmd/auth/mode/set") == 0)
        handle_cmd_auth_mode_set(data);
    else if (strcmp(sub, "cmd/auth/tags/add") == 0)
        handle_cmd_auth_tag_add(data);
    else if (strcmp(sub, "cmd/auth/tags/remove") == 0)
        handle_cmd_auth_tag_remove(data);
    else if (strcmp(sub, "cmd/scheduler/set") == 0)
        handle_cmd_scheduler_set(data);
    else if (strcmp(sub, "cmd/loadbalancing/set") == 0)
        handle_cmd_loadbalancing_set(data);
    else if (strcmp(sub, "cmd/meters/set") == 0)
        handle_cmd_meters_set(data);
    else if (strcmp(sub, "cmd/evse/settings") == 0)
        handle_cmd_evse_settings(data);
    else if (strcmp(sub, "cmd/evse/action") == 0)
        handle_cmd_evse_action(data);
    else if (strcmp(sub, "cmd/evse/limit") == 0)
        handle_cmd_evse_limit(data);
    else
        ESP_LOGW(TAG, "MQTT command in unknown topic: %s", topic);
}

static esp_err_t mqtt_event_handler_cb(esp_mqtt_event_handle_t event)
{
    (void)event->client;

    switch (event->event_id)
    {
    case MQTT_EVENT_CONNECTED:
        ESP_LOGI(TAG, "MQTT_EVENT_CONNECTED");
        s_connected = true;
        mqtt_subscribe_all();

        mqtt_publish_raw("state/availability", "online", true);

        mqtt_auth_publish_state();
        mqtt_scheduler_publish_state();
        mqtt_loadbalancer_publish_state();
        mqtt_meters_publish_config();
        mqtt_evse_publish_state();
        mqtt_limit_publish_state();
        mqtt_session_publish_state();
        mqtt_system_publish_heartbeat();
        break;

    case MQTT_EVENT_DISCONNECTED:
        ESP_LOGI(TAG, "MQTT_EVENT_DISCONNECTED");
        s_connected = false;
        break;

    case MQTT_EVENT_DATA:
    {
        int tlen = event->topic_len;
        int dlen = event->data_len;

        if (tlen <= 0 || dlen <= 0 || !event->topic || !event->data)
            break;

        char *topic = (char *)malloc((size_t)tlen + 1);
        char *payload = (char *)malloc((size_t)dlen + 1);
        if (!topic || !payload)
        {
            free(topic);
            free(payload);
            ESP_LOGE(TAG, "MQTT_EVENT_DATA: malloc failed");
            break;
        }

        memcpy(topic, event->topic, (size_t)tlen);
        topic[tlen] = '\0';

        memcpy(payload, event->data, (size_t)dlen);
        payload[dlen] = '\0';

        ESP_LOGI(TAG, "MQTT DATA topic='%s' payload='%s'", topic, payload);
        mqtt_dispatch_command(topic, payload);

        free(topic);
        free(payload);
        break;
    }

    default:
        break;
    }

    return ESP_OK;
}

static void mqtt_event_handler(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data)
{
    (void)handler_args;
    (void)base;
    (void)event_id;
    mqtt_event_handler_cb((esp_mqtt_event_handle_t)event_data);
}

static void mqtt_client_start_if_needed(void)
{
    if (!s_mqtt_enabled)
    {
        ESP_LOGI(TAG, "MQTT disabled, not starting client");
        return;
    }

    if (s_client)
    {
        ESP_LOGI(TAG, "MQTT client already created");
        return;
    }

    if (s_mqtt_host[0] == '\0')
    {
        ESP_LOGW(TAG, "MQTT host not set");
        return;
    }

    char uri[MQTT_HOST_MAX_LEN + 10];

    if (strncmp(s_mqtt_host, "mqtt://", 7) == 0 ||
        strncmp(s_mqtt_host, "mqtts://", 8) == 0)
    {
        strncpy(uri, s_mqtt_host, sizeof(uri));
        uri[sizeof(uri) - 1] = '\0';
    }
    else
    {
        snprintf(uri, sizeof(uri), "mqtt://%s", s_mqtt_host);
    }

    if (s_client_id[0] == '\0')
    {
        uint8_t mac[6] = {0};
        esp_read_mac(mac, ESP_MAC_WIFI_STA);
        snprintf(s_client_id, sizeof(s_client_id),
                 "evse-%02x%02x%02x%02x%02x%02x",
                 mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
    }

    static char lwt_topic[MQTT_TOPIC_BUF_LEN];
    mqtt_build_topic(lwt_topic, sizeof(lwt_topic), "state/availability");

    esp_mqtt_client_config_t cfg = {
        .broker.address.uri = uri,
        .credentials.client_id = s_client_id,
        .session.last_will = {
            .topic = lwt_topic,
            .msg = "offline",
            .msg_len = 0,
            .qos = 1,
            .retain = 1,
        },
    };

    if (s_mqtt_user[0] != '\0')
        cfg.credentials.username = s_mqtt_user;
    if (s_mqtt_pass[0] != '\0')
        cfg.credentials.authentication.password = s_mqtt_pass;

    s_client = esp_mqtt_client_init(&cfg);
    if (!s_client)
    {
        ESP_LOGE(TAG, "esp_mqtt_client_init failed");
        return;
    }

    esp_mqtt_client_register_event(s_client, ESP_EVENT_ANY_ID, mqtt_event_handler, NULL);

    esp_err_t err = esp_mqtt_client_start(s_client);
    if (err != ESP_OK)
    {
        ESP_LOGE(TAG, "esp_mqtt_client_start failed: %s", esp_err_to_name(err));
        esp_mqtt_client_destroy(s_client);
        s_client = NULL;
    }
}

static void mqtt_client_stop(void)
{
    if (!s_client)
        return;

    esp_mqtt_client_stop(s_client);
    esp_mqtt_client_destroy(s_client);
    s_client = NULL;
    s_connected = false;
}

static void mqtt_network_event_handler(void *arg, esp_event_base_t base, int32_t id, void *data)
{
    (void)arg;
    (void)data;
    if (base != NETWORK_EVENTS)
        return;

    switch (id)
    {
    case NETWORK_EVENT_STA_GOT_IP:
        ESP_LOGI(TAG, "NETWORK_EVENT_STA_GOT_IP -> tentar ligar MQTT");
        mqtt_client_start_if_needed();
        break;
    default:
        break;
    }
}

static void mqtt_telemetry_task(void *arg)
{
    (void)arg;
    TickType_t last_wake = xTaskGetTickCount();
    uint32_t heartbeat_counter = 0;

    while (1)
    {
        uint16_t period = s_mqtt_periodicity;
        if (period == 0)
            period = 30;

        if (s_mqtt_enabled && s_client && s_connected)
        {
            mqtt_evse_publish_state();
            mqtt_session_publish_state();

            if ((heartbeat_counter++ % 5) == 0)
                mqtt_system_publish_heartbeat();
        }

        vTaskDelayUntil(&last_wake, pdMS_TO_TICKS(period * 1000));
    }
}

esp_err_t mqtt_init(void)
{
    mqtt_load_config_from_storage();

    ESP_ERROR_CHECK(esp_event_handler_register(AUTH_EVENTS, ESP_EVENT_ANY_ID, mqtt_auth_event_handler, NULL));
    ESP_ERROR_CHECK(esp_event_handler_register(SCHED_EVENTS, ESP_EVENT_ANY_ID, mqtt_scheduler_event_handler, NULL));
    ESP_ERROR_CHECK(esp_event_handler_register(LOADBALANCER_EVENTS, ESP_EVENT_ANY_ID, mqtt_loadbalancer_event_handler, NULL));
    ESP_ERROR_CHECK(esp_event_handler_register(METER_EVENT, ESP_EVENT_ANY_ID, mqtt_meter_event_handler, NULL));
    ESP_ERROR_CHECK(esp_event_handler_register(EVSE_EVENTS, ESP_EVENT_ANY_ID, mqtt_evse_event_handler, NULL));
    ESP_ERROR_CHECK(esp_event_handler_register(NETWORK_EVENTS, ESP_EVENT_ANY_ID, mqtt_network_event_handler, NULL));

    if (!s_telemetry_task)
    {
        xTaskCreate(mqtt_telemetry_task, "mqtt_telemetry_task", 4096, NULL, 3, &s_telemetry_task);
    }

    mqtt_apply_limit_mode(s_limit_mode);
    return ESP_OK;
}

esp_err_t mqtt_start(void)
{
    mqtt_client_start_if_needed();
    return ESP_OK;
}

void mqtt_stop(void)
{
    mqtt_client_stop();
}

esp_err_t mqtt_set_config(bool enabled,
                          const char *host,
                          const char *topic,
                          const char *username,
                          const char *password,
                          int periodicity)
{
    bool reconnect_needed = s_connected || s_client;
    if (reconnect_needed)
        mqtt_client_stop();

    s_mqtt_enabled = enabled;

    if (host)
        strncpy(s_mqtt_host, host, sizeof(s_mqtt_host));
    s_mqtt_host[sizeof(s_mqtt_host) - 1] = '\0';

    if (topic)
        strncpy(s_mqtt_base_topic, topic, sizeof(s_mqtt_base_topic));
    s_mqtt_base_topic[sizeof(s_mqtt_base_topic) - 1] = '\0';

    if (username)
        strncpy(s_mqtt_user, username, sizeof(s_mqtt_user));
    s_mqtt_user[sizeof(s_mqtt_user) - 1] = '\0';

    if (password)
        strncpy(s_mqtt_pass, password, sizeof(s_mqtt_pass));
    s_mqtt_pass[sizeof(s_mqtt_pass) - 1] = '\0';

    if (periodicity > 0 && periodicity < 3600)
        s_mqtt_periodicity = (uint16_t)periodicity;

    esp_err_t err = mqtt_save_config_to_storage();
    if (err != ESP_OK)
        return err;

    if (s_mqtt_enabled)
        mqtt_client_start_if_needed();

    return ESP_OK;
}