// components/loadbalancer/src/loadbalancer.c

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

#include "pv_optimizer.h"
#include "grid_limiter.h"

#include "esp_event.h"
#include "esp_log.h"
#include "esp_timer.h"
#include "esp_err.h"

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

#include "input_filter.h"
#include "meter_events.h"
#include "evse_events.h"

#include "storage_service.h"
#include "evse_link_events.h"

#include <string.h>
#include <math.h>
#include <inttypes.h>

#ifndef MIN
#define MIN(a, b) ((a) < (b) ? (a) : (b))
#endif

static const char *TAG = "loadbalancer";

#define MIN_CHARGING_CURRENT_LIMIT 6
#define MAX_CHARGING_CURRENT_LIMIT 32

#define MIN_GRID_CURRENT_LIMIT 6
#define MAX_GRID_CURRENT_LIMIT 100

#define AVAILABLE_EPS 1.0f
#define LB_SUSPEND_THRESHOLD (MIN_CHARGING_CURRENT_LIMIT - 1.0f)
#define LB_RESUME_THRESHOLD (MIN_CHARGING_CURRENT_LIMIT + 1.0f)

#define GRID_METER_TIMEOUT_US (10LL * 1000000LL)

static bool loadbalancer_enabled = false;

// GRID limit
static bool grid_limit_enabled = true;
static uint8_t max_grid_current = MAX_GRID_CURRENT_LIMIT; // mantém nome (já usado em helpers)

// PV
static bool pv_enabled = false;
static int32_t pv_max_import_w = 0; // 0=só PV

// métricas do meter
static float grid_current = 0.0f; // fallback magnitude (max irms filtrado)
static input_filter_t grid_filter;

static input_filter_t grid_power_filter; // NEW
static int32_t grid_watt_total = 0;      // NEW (filtrado)
static meter_event_data_t last_grid_evt; // NEW
static bool have_grid_evt = false;       // NEW

static int64_t last_grid_timestamp_us = 0;

#define MAX_SLAVES 30
#define CONNECTOR_COUNT (MAX_SLAVES + 1)

static SemaphoreHandle_t lb_mutex = NULL;

typedef struct
{
    uint8_t id; // slaves: 0..254
    bool is_master;

    bool charging;

    float hw_max_current;
    float runtime_current;

    int64_t timestamp; // última atualização de métricas
    bool online;       // fonte: EVSE-Link (ou status recebido)

    float assigned;
    int64_t started_us;
    uint16_t last_limit;

    bool suspended_by_lb;
} evse_connector_t;

static evse_connector_t connectors[CONNECTOR_COUNT];

static const int64_t METRICS_TIMEOUT_US = 60LL * 1000000LL;

#define LB_NS "loadbalancing"
#define LB_KEY_ENABLED "enabled"
#define LB_KEY_GRID_ON "grid_on"
#define LB_KEY_GRID_MAXA "grid_max_a"
#define LB_KEY_PV_ON "pv_on"
#define LB_KEY_PV_MAXW "pv_max_w" // precisa storage u32/i32

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

static void input_filter_reset(input_filter_t *filter)
{
    if (!filter)
        return;
    filter->value = 0.0f;
    filter->initialized = 0;
}

static void init_connectors(void)
{
    connectors[0] = (evse_connector_t){
        .id = 0,
        .is_master = true,
        .charging = false,
        .hw_max_current = MAX_CHARGING_CURRENT_LIMIT,
        .runtime_current = 0,
        .timestamp = 0,
        .online = true,
        .assigned = 0.0f,
        .started_us = 0,
        .last_limit = 0,
        .suspended_by_lb = false};

    for (int i = 1; i < CONNECTOR_COUNT; i++)
    {
        connectors[i] = (evse_connector_t){
            .id = (uint8_t)(i - 1),
            .is_master = false,
            .charging = false,
            .hw_max_current = 0.0f,
            .runtime_current = 0.0f,
            .timestamp = 0,
            .online = false,
            .assigned = 0.0f,
            .started_us = 0,
            .last_limit = 0,
            .suspended_by_lb = false};
    }
}

// ===== EVSE-Link presence -> connectors[].online =====
static void on_evse_link_presence(void *handler_arg, esp_event_base_t base, int32_t id, void *data)
{
    (void)handler_arg;
    if (base != EVSE_LINK_EVENTS)
        return;
    if (id != LINK_EVENT_SLAVE_ONLINE && id != LINK_EVENT_SLAVE_OFFLINE)
        return;

    uint8_t sid = 0xFF;
    if (!data)
        return;

    const evse_link_slave_presence_event_t *p = (const evse_link_slave_presence_event_t *)data;
    sid = p->slave_id;
    if (sid >= MAX_SLAVES)
        return;

    const int idx = (int)sid + 1;

    if (lb_mutex)
        xSemaphoreTake(lb_mutex, portMAX_DELAY);

    if (id == LINK_EVENT_SLAVE_ONLINE)
    {
        connectors[idx].online = true;
        ESP_LOGI(TAG, "Slave %u marked ONLINE (EVSE-Link)", (unsigned)sid);
    }
    else
    {
        connectors[idx].online = false;
        connectors[idx].charging = false;
        connectors[idx].assigned = 0.0f;
        connectors[idx].suspended_by_lb = false;
        connectors[idx].last_limit = 0;
        ESP_LOGW(TAG, "Slave %u marked OFFLINE (EVSE-Link)", (unsigned)sid);
    }

    if (lb_mutex)
        xSemaphoreGive(lb_mutex);
}

static void on_slave_status(void *handler_arg, esp_event_base_t base, int32_t id, void *data)
{
    (void)handler_arg;
    if (base != LOADBALANCER_EVENTS || id != LOADBALANCER_EVENT_SLAVE_STATUS || !data)
        return;

    const loadbalancer_slave_status_event_t *status = (const loadbalancer_slave_status_event_t *)data;

    if (status->slave_id >= MAX_SLAVES)
    {
        ESP_LOGW(TAG, "Invalid slave_id %u", (unsigned)status->slave_id);
        return;
    }

    const int idx = (int)status->slave_id + 1;

    if (lb_mutex)
        xSemaphoreTake(lb_mutex, portMAX_DELAY);

    bool was_charging = connectors[idx].charging;

    connectors[idx].charging = status->charging;
    connectors[idx].hw_max_current = status->hw_max_current;
    connectors[idx].runtime_current = status->runtime_current;
    connectors[idx].timestamp = (status->timestamp_us > 0) ? status->timestamp_us : esp_timer_get_time();
    connectors[idx].online = true;

    if (status->charging && !was_charging)
    {
        connectors[idx].started_us = connectors[idx].timestamp;
        connectors[idx].suspended_by_lb = false;
    }

    if (lb_mutex)
        xSemaphoreGive(lb_mutex);

    ESP_LOGD(TAG, "Slave %u status: charging=%d hw=%.1fA runtime=%.2fA",
             (unsigned)status->slave_id, status->charging,
             status->hw_max_current, status->runtime_current);
}

static void on_evse_config_event(void *handler_arg, esp_event_base_t base, int32_t id, void *event_data)
{
    (void)handler_arg;
    if (base != EVSE_EVENTS || id != EVSE_EVENT_CONFIG_UPDATED || !event_data)
        return;

    const evse_config_event_data_t *evt = (const evse_config_event_data_t *)event_data;

    if (lb_mutex)
        xSemaphoreTake(lb_mutex, portMAX_DELAY);

    connectors[0].charging = evt->charging;
    connectors[0].hw_max_current = evt->hw_max_current;
    connectors[0].runtime_current = evt->runtime_current;
    connectors[0].timestamp = esp_timer_get_time();
    connectors[0].online = true;

    if (!evt->charging)
        connectors[0].suspended_by_lb = false;

    if (lb_mutex)
        xSemaphoreGive(lb_mutex);
}

static void loadbalancer_meter_event_handler(void *handler_arg, esp_event_base_t base, int32_t id, void *event_data)
{
    (void)handler_arg;
    (void)base;

    if (id != METER_EVENT_DATA_READY || !event_data)
        return;

    const meter_event_data_t *evt = (const meter_event_data_t *)event_data;

    float max_irms = evt->irms[0];
    for (int i = 1; i < 3; ++i)
        if (evt->irms[i] > max_irms)
            max_irms = evt->irms[i];

    if (lb_mutex)
        xSemaphoreTake(lb_mutex, portMAX_DELAY);

    if (evt->source && strcmp(evt->source, "GRID") == 0)
    {
        grid_current = input_filter_update(&grid_filter, max_irms);

        // NEW: guardar evento completo + potência filtrada
        last_grid_evt = *evt;

        // timestamp preferido do evento
        const int64_t ts = (evt->timestamp_us > 0) ? evt->timestamp_us : esp_timer_get_time();
        last_grid_timestamp_us = ts;
        last_grid_evt.timestamp_us = ts;

        float p_f = input_filter_update(&grid_power_filter, (float)evt->watt_total);
        grid_watt_total = (int32_t)lroundf(p_f);
        last_grid_evt.watt_total = grid_watt_total;

        have_grid_evt = true;

        ESP_LOGI(TAG, "GRID: I=%.3fA P=%ldW (maxA=%u pv_on=%d pvMaxW=%ld)",
                 grid_current, (long)grid_watt_total, (unsigned)max_grid_current,
                 pv_enabled, (long)pv_max_import_w);
    }
    else
    {
        // EVSE meter ignorado por agora (como combinaste)
    }

    if (lb_mutex)
        xSemaphoreGive(lb_mutex);
}

static void loadbalancer_evse_event_handler(void *handler_arg, esp_event_base_t base, int32_t id, void *event_data)
{
    (void)handler_arg;
    if (base != EVSE_EVENTS || id != EVSE_EVENT_STATE_CHANGED || !event_data)
        return;

    const evse_state_event_data_t *evt = (const evse_state_event_data_t *)event_data;

    if (lb_mutex)
        xSemaphoreTake(lb_mutex, portMAX_DELAY);

    switch (evt->state)
    {
    case EVSE_STATE_EVENT_IDLE:
    case EVSE_STATE_EVENT_WAITING:
        connectors[0].charging = false;
        connectors[0].online = true;
        connectors[0].suspended_by_lb = false;
        break;

    case EVSE_STATE_EVENT_CHARGING:
    {
        grid_current = 0.0f;
        input_filter_reset(&grid_filter);
        input_filter_reset(&grid_power_filter);

        bool was_charging = connectors[0].charging;

        connectors[0].charging = true;
        connectors[0].online = true;
        connectors[0].timestamp = esp_timer_get_time();

        if (!was_charging)
        {
            connectors[0].started_us = connectors[0].timestamp;
            connectors[0].suspended_by_lb = false;
        }
        break;
    }

    case EVSE_STATE_EVENT_FAULT:
        connectors[0].charging = false;
        connectors[0].online = true;
        connectors[0].suspended_by_lb = false;
        break;

    default:
        break;
    }

    if (lb_mutex)
        xSemaphoreGive(lb_mutex);
}

// --------- Config load/save ---------
static esp_err_t loadbalancer_load_config(void)
{
    esp_err_t se = storage_service_init();
    if (se != ESP_OK)
        return se;

    esp_err_t err;
    bool needs_flush = false;
    uint8_t temp_u8 = 0;

    // enabled
    err = storage_get_u8_sync(LB_NS, LB_KEY_ENABLED, &temp_u8, TO_TICKS_MS(800));
    if (err == ESP_OK && temp_u8 <= 1)
        loadbalancer_enabled = (temp_u8 != 0);
    else
    {
        loadbalancer_enabled = false;
        (void)storage_set_u8_async(LB_NS, LB_KEY_ENABLED, 0);
        needs_flush = true;
    }

    // grid on
    err = storage_get_u8_sync(LB_NS, LB_KEY_GRID_ON, &temp_u8, TO_TICKS_MS(800));
    if (err == ESP_OK && temp_u8 <= 1)
        grid_limit_enabled = (temp_u8 != 0);
    else
    {
        grid_limit_enabled = true;
        (void)storage_set_u8_async(LB_NS, LB_KEY_GRID_ON, 1);
        needs_flush = true;
    }

    // grid maxA
    err = storage_get_u8_sync(LB_NS, LB_KEY_GRID_MAXA, &temp_u8, TO_TICKS_MS(800));
    if (err == ESP_OK && temp_u8 >= MIN_GRID_CURRENT_LIMIT && temp_u8 <= MAX_GRID_CURRENT_LIMIT)
        max_grid_current = temp_u8;
    else
    {
        max_grid_current = MAX_GRID_CURRENT_LIMIT;
        (void)storage_set_u8_async(LB_NS, LB_KEY_GRID_MAXA, max_grid_current);
        needs_flush = true;
    }

    // pv on
    err = storage_get_u8_sync(LB_NS, LB_KEY_PV_ON, &temp_u8, TO_TICKS_MS(800));
    if (err == ESP_OK && temp_u8 <= 1)
        pv_enabled = (temp_u8 != 0);
    else
    {
        pv_enabled = false;
        (void)storage_set_u8_async(LB_NS, LB_KEY_PV_ON, 0);
        needs_flush = true;
    }

    // pv maxW (se não tiveres storage_get_u32_sync, adapta para i32/u32 do teu storage_service)
    uint32_t temp_u32 = 0;
    err = storage_get_u32_sync(LB_NS, LB_KEY_PV_MAXW, &temp_u32, TO_TICKS_MS(800));
    if (err == ESP_OK)
        pv_max_import_w = (int32_t)temp_u32;
    else
    {
        pv_max_import_w = 0;
        (void)storage_set_u32_async(LB_NS, LB_KEY_PV_MAXW, 0);
        needs_flush = true;
    }

    if (needs_flush)
        (void)storage_flush_sync(TO_TICKS_MS(2000));

    // propagar para sub-módulos
    pv_optimizer_set_enabled(pv_enabled);
    (void)pv_optimizer_set_max_import_w(pv_max_import_w);

    grid_limiter_set_enabled(grid_limit_enabled);
    (void)grid_limiter_set_max_import_a(max_grid_current);

    return ESP_OK;
}

static void persist_u8(const char *key, uint8_t v) { (void)storage_set_u8_async(LB_NS, key, v); }
static void persist_u32(const char *key, uint32_t v) { (void)storage_set_u32_async(LB_NS, key, v); }

void loadbalancer_set_enabled(bool enabled)
{
    if (enabled == loadbalancer_enabled)
        return;

    loadbalancer_enabled = enabled;
    persist_u8(LB_KEY_ENABLED, enabled ? 1 : 0);

    loadbalancer_state_event_t evt = {.enabled = enabled, .timestamp_us = esp_timer_get_time()};
    (void)esp_event_post(LOADBALANCER_EVENTS, LOADBALANCER_EVENT_STATE_CHANGED,
                         &evt, sizeof(evt), portMAX_DELAY);
}

bool loadbalancer_is_enabled(void) { return loadbalancer_enabled; }

// grid setters/getters
void loadbalancer_grid_set_enabled(bool en)
{
    grid_limit_enabled = en;
    persist_u8(LB_KEY_GRID_ON, en ? 1 : 0);
    grid_limiter_set_enabled(en);
}

bool loadbalancer_grid_is_enabled(void) { return grid_limit_enabled; }

esp_err_t loadbalancer_grid_set_max_import_a(uint8_t a)
{
    if (a < MIN_GRID_CURRENT_LIMIT || a > MAX_GRID_CURRENT_LIMIT)
        return ESP_ERR_INVALID_ARG;
    max_grid_current = a;
    persist_u8(LB_KEY_GRID_MAXA, a);
    return grid_limiter_set_max_import_a(a);
}

uint8_t loadbalancer_grid_get_max_import_a(void) { return max_grid_current; }

// pv setters/getters
void loadbalancer_pv_set_enabled(bool en)
{
    pv_enabled = en;
    persist_u8(LB_KEY_PV_ON, en ? 1 : 0);
    pv_optimizer_set_enabled(en);
}

bool loadbalancer_pv_is_enabled(void) { return pv_enabled; }

esp_err_t loadbalancer_pv_set_max_import_w(int32_t w)
{
    esp_err_t e = pv_optimizer_set_max_import_w(w);
    if (e != ESP_OK)
        return e;

    pv_max_import_w = w;
    persist_u32(LB_KEY_PV_MAXW, (uint32_t)w);
    return ESP_OK;
}

int32_t loadbalancer_pv_get_max_import_w(void) { return pv_max_import_w; }

// aliases legacy
esp_err_t load_balancing_set_max_grid_current(uint8_t value) { return loadbalancer_grid_set_max_import_a(value); }
uint8_t load_balancing_get_max_grid_current(void) { return loadbalancer_grid_get_max_import_a(); }

// ===== util: distribuição “water-fill” segura =====
static void distribute_extra_evenly(int *idxs, int cnt, float extra)
{
    if (cnt <= 0 || extra <= 0.0f)
        return;

    while (extra > AVAILABLE_EPS)
    {
        int eligible = 0;
        for (int k = 0; k < cnt; k++)
        {
            int i = idxs[k];
            float headroom = connectors[i].hw_max_current - connectors[i].assigned;
            if (headroom > AVAILABLE_EPS && connectors[i].assigned > 0.0f)
                eligible++;
        }

        if (eligible <= 0)
            break;

        float share = extra / (float)eligible;
        if (share <= AVAILABLE_EPS)
            break;

        for (int k = 0; k < cnt; k++)
        {
            int i = idxs[k];
            if (connectors[i].assigned <= 0.0f)
                continue;

            float headroom = connectors[i].hw_max_current - connectors[i].assigned;
            if (headroom <= AVAILABLE_EPS)
                continue;

            float inc = MIN(share, headroom);
            connectors[i].assigned += inc;
            extra -= inc;

            if (extra <= AVAILABLE_EPS)
                break;
        }
    }
}

// ============================================================================
// helpers (iguais aos teus) + NOVO: budget policy
// ============================================================================

typedef enum
{
    LB_LIMIT_EVT_MASTER = 0,
    LB_LIMIT_EVT_SLAVE = 1
} lb_limit_evt_type_t;

typedef struct
{
    lb_limit_evt_type_t type;
    uint8_t slave_id;
    uint16_t max_current;
    int64_t timestamp_us;
} lb_pending_limit_t;

static int build_active_list_locked(int64_t now, int *idxs, int idxs_max)
{
    int n = 0;
    for (int i = 0; i < CONNECTOR_COUNT && n < idxs_max; i++)
    {
        if (!connectors[i].online)
            continue;
        if (!connectors[i].charging)
            continue;

        if (!connectors[i].is_master)
        {
            if (connectors[i].timestamp == 0 || (now - connectors[i].timestamp) >= METRICS_TIMEOUT_US)
                continue;
        }

        idxs[n++] = i;
    }
    return n;
}

static void sort_active_by_started_locked(int *idxs, int cnt)
{
    for (int a = 0; a < cnt - 1; a++)
        for (int b = 0; b < cnt - 1 - a; b++)
            if (connectors[idxs[b]].started_us > connectors[idxs[b + 1]].started_us)
            {
                int tmp = idxs[b];
                idxs[b] = idxs[b + 1];
                idxs[b + 1] = tmp;
            }
}

static float sum_hw_max_locked(const int *idxs, int cnt)
{
    float s = 0.0f;
    for (int k = 0; k < cnt; k++)
    {
        int i = idxs[k];
        if (connectors[i].hw_max_current > 0.0f)
            s += connectors[i].hw_max_current;
    }
    return s;
}

static float sum_last_limit_locked(const int *idxs, int cnt)
{
    float s = 0.0f;
    for (int k = 0; k < cnt; k++)
    {
        int i = idxs[k];
        s += (float)connectors[i].last_limit;
    }
    return s;
}

static float sum_assigned_locked(const int *idxs, int cnt)
{
    float s = 0.0f;
    for (int k = 0; k < cnt; k++)
    {
        int i = idxs[k];
        if (connectors[i].assigned > 0.0f)
            s += connectors[i].assigned;
    }
    return s;
}

// NEW: política por budget total (A)
static void apply_budget_policy_locked(int *idxs, int cnt, float total_budget_a)
{
    if (cnt <= 0)
        return;
    if (total_budget_a < 0.0f)
        total_budget_a = 0.0f;

    // baseline: tudo a 0
    for (int k = 0; k < cnt; k++)
    {
        int i = idxs[k];
        connectors[i].assigned = 0.0f;
        connectors[i].suspended_by_lb = true;
    }

    // mínimos por conector (respeita hw_max)
    float required = 0.0f;
    for (int k = 0; k < cnt; k++)
    {
        int i = idxs[k];
        float hw = connectors[i].hw_max_current;
        float min_i = MIN((float)MIN_CHARGING_CURRENT_LIMIT, hw);
        if (min_i > 0.0f)
            required += min_i;
    }

    float remaining = total_budget_a;

    if (remaining + AVAILABLE_EPS >= required)
    {
        // dá para dar mínimo a todos
        for (int k = 0; k < cnt; k++)
        {
            int i = idxs[k];
            float hw = connectors[i].hw_max_current;
            float min_i = MIN((float)MIN_CHARGING_CURRENT_LIMIT, hw);
            if (min_i > 0.0f)
            {
                connectors[i].assigned = min_i;
                connectors[i].suspended_by_lb = false;
                remaining -= min_i;
            }
        }

        if (remaining > AVAILABLE_EPS)
            distribute_extra_evenly(idxs, cnt, remaining);
    }
    else
    {
        // não dá: mínimos só para os mais antigos até caber
        for (int k = 0; k < cnt; k++)
        {
            int i = idxs[k];
            float hw = connectors[i].hw_max_current;
            float min_i = MIN((float)MIN_CHARGING_CURRENT_LIMIT, hw);

            if (min_i > 0.0f && remaining + AVAILABLE_EPS >= min_i)
            {
                connectors[i].assigned = min_i;
                connectors[i].suspended_by_lb = false;
                remaining -= min_i;
            }
        }
    }
}

static void apply_meter_timeout_policy_locked(int *idxs, int cnt)
{
    // igual ao teu: tenta manter MIN se couber no max_grid_current
    const float budget = (float)max_grid_current;
    float need = (float)MIN_CHARGING_CURRENT_LIMIT * (float)cnt;

    if (need <= budget + AVAILABLE_EPS)
    {
        for (int k = 0; k < cnt; k++)
        {
            int i = idxs[k];
            float hw = connectors[i].hw_max_current;
            float target = MIN((float)MIN_CHARGING_CURRENT_LIMIT, hw);
            connectors[i].assigned = target;
            connectors[i].suspended_by_lb = false;
        }
    }
    else
    {
        int can = (int)floorf(budget / (float)MIN_CHARGING_CURRENT_LIMIT);
        if (can < 0)
            can = 0;
        if (can > cnt)
            can = cnt;

        for (int k = 0; k < cnt; k++)
        {
            int i = idxs[k];
            if (k < can)
            {
                float hw = connectors[i].hw_max_current;
                connectors[i].assigned = MIN((float)MIN_CHARGING_CURRENT_LIMIT, hw);
                connectors[i].suspended_by_lb = false;
            }
            else
            {
                connectors[i].assigned = 0.0f;
                connectors[i].suspended_by_lb = true;
            }
        }
    }
}

static void apply_normal_policy_locked(int *idxs, int cnt, float grid_snapshot)
{
    // o teu algoritmo atual (grid-limit em A, por corrente medida)
    float available = (float)max_grid_current - grid_snapshot;

    if (available < -AVAILABLE_EPS)
    {
        float factor = 0.0f;
        if (grid_snapshot > AVAILABLE_EPS)
            factor = ((float)max_grid_current) / grid_snapshot;

        if (factor < 0.0f)
            factor = 0.0f;
        if (factor > 1.0f)
            factor = 1.0f;

        for (int k = 0; k < cnt; k++)
        {
            int i = idxs[k];
            // baseline = runtime (como tinhas)
            float cur = connectors[i].runtime_current;
            float hw = connectors[i].hw_max_current;
            if (cur < 0.0f)
                cur = 0.0f;
            if (hw < 0.0f)
                hw = 0.0f;
            connectors[i].assigned = MIN(cur, hw) * factor;
        }
        return;
    }

    // baseline = runtime
    for (int k = 0; k < cnt; k++)
    {
        int i = idxs[k];
        float cur = connectors[i].runtime_current;
        float hw = connectors[i].hw_max_current;
        if (cur < 0.0f)
            cur = 0.0f;
        if (hw < 0.0f)
            hw = 0.0f;
        connectors[i].assigned = MIN(cur, hw);
    }

    if (fabsf(available) <= AVAILABLE_EPS)
        return;

    float remaining = available;

    // fase 1: subir até MIN para os mais antigos, se possível
    for (int k = 0; k < cnt && remaining > 0.0f; k++)
    {
        int i = idxs[k];
        float hw = connectors[i].hw_max_current;
        float target = MIN((float)MIN_CHARGING_CURRENT_LIMIT, hw);

        if (connectors[i].assigned >= target)
            continue;

        float delta = target - connectors[i].assigned;
        float inc = MIN(delta, remaining);
        connectors[i].assigned += inc;
        remaining -= inc;
    }

    // fase 2: suspender os mais recentes que ficaram abaixo do mínimo
    for (int k = cnt - 1; k >= 0; k--)
    {
        int i = idxs[k];
        if (connectors[i].assigned >= (float)MIN_CHARGING_CURRENT_LIMIT)
            continue;

        connectors[i].assigned = 0.0f;
        connectors[i].suspended_by_lb = true;
    }

    // fase 3: distribuir extra pelos que estão ativos
    if (remaining > AVAILABLE_EPS)
        distribute_extra_evenly(idxs, cnt, remaining);
}

static int prepare_pending_limits_locked(const int *idxs,
                                         int cnt,
                                         int64_t now,
                                         lb_pending_limit_t *out,
                                         int out_max)
{
    int out_cnt = 0;

    for (int k = 0; k < cnt; k++)
    {
        int i = idxs[k];

        float assigned = connectors[i].assigned;
        float effective = assigned;

        // histerese suspend/resume (mantida)
        if (connectors[i].suspended_by_lb)
        {
            if (assigned >= LB_RESUME_THRESHOLD)
            {
                effective = assigned;
                connectors[i].suspended_by_lb = false;
            }
            else
            {
                effective = 0.0f;
            }
        }
        else
        {
            if (assigned > 0.0f && assigned < LB_SUSPEND_THRESHOLD)
            {
                effective = 0.0f;
                connectors[i].suspended_by_lb = true;
            }
        }

        uint16_t max_cur = (effective <= 0.0f) ? 0 : (uint16_t)MIN(effective, (float)MAX_CHARGING_CURRENT_LIMIT);

        if (connectors[i].last_limit == max_cur)
            continue;

        connectors[i].last_limit = max_cur;

        if (out_cnt < out_max)
        {
            lb_pending_limit_t p = {
                .type = connectors[i].is_master ? LB_LIMIT_EVT_MASTER : LB_LIMIT_EVT_SLAVE,
                .slave_id = connectors[i].is_master ? 0 : connectors[i].id,
                .max_current = max_cur,
                .timestamp_us = now};
            out[out_cnt++] = p;
        }
    }

    return out_cnt;
}

static void post_pending_limits(const lb_pending_limit_t *pending, int pending_cnt)
{
    for (int i = 0; i < pending_cnt; i++)
    {
        const lb_pending_limit_t *p = &pending[i];

        if (p->type == LB_LIMIT_EVT_MASTER)
        {
            loadbalancer_master_limit_event_t m = {.max_current = p->max_current, .timestamp_us = p->timestamp_us};
            (void)esp_event_post(LOADBALANCER_EVENTS, LOADBALANCER_EVENT_MASTER_CURRENT_LIMIT,
                                 &m, sizeof(m), portMAX_DELAY);
        }
        else
        {
            if (p->slave_id < MAX_SLAVES)
            {
                loadbalancer_slave_limit_event_t s = {.slave_id = p->slave_id, .max_current = p->max_current, .timestamp_us = p->timestamp_us};
                (void)esp_event_post(LOADBALANCER_EVENTS, LOADBALANCER_EVENT_SLAVE_CURRENT_LIMIT,
                                     &s, sizeof(s), portMAX_DELAY);
            }
        }
    }
}

// ===== loadbalancer_task (PV + grid cap, event-driven) =====
void loadbalancer_task(void *param)
{
    (void)param;

    while (true)
    {
        if (!loadbalancer_is_enabled())
        {
            vTaskDelay(pdMS_TO_TICKS(30000));
            continue;
        }

        const int64_t now = esp_timer_get_time();

        int idxs[CONNECTOR_COUNT];
        int active_cnt = 0;

        float grid_snapshot = 0.0f;
        int64_t last_grid_ts_snapshot = 0;
        bool have_grid_snapshot = false;
        meter_event_data_t grid_evt_snapshot;

        // 1) snapshot + lista ativa
        if (lb_mutex)
            xSemaphoreTake(lb_mutex, portMAX_DELAY);

        grid_snapshot = grid_current;
        last_grid_ts_snapshot = last_grid_timestamp_us;

        have_grid_snapshot = have_grid_evt;
        grid_evt_snapshot = last_grid_evt;

        active_cnt = build_active_list_locked(now, idxs, CONNECTOR_COUNT);

        if (lb_mutex)
            xSemaphoreGive(lb_mutex);

        if (active_cnt == 0)
        {
            vTaskDelay(pdMS_TO_TICKS(5000));
            continue;
        }

        bool meter_timeout = (last_grid_ts_snapshot == 0 ||
                              (now - last_grid_ts_snapshot) > GRID_METER_TIMEOUT_US);

        // 2) calcular assigned + preparar eventos
        lb_pending_limit_t pending[CONNECTOR_COUNT];
        int pending_cnt = 0;

        if (lb_mutex)
            xSemaphoreTake(lb_mutex, portMAX_DELAY);

        sort_active_by_started_locked(idxs, active_cnt);

        const float total_hw_max_a = sum_hw_max_locked(idxs, active_cnt);
        const float last_total_cmd_a = sum_last_limit_locked(idxs, active_cnt);

        if (meter_timeout)
        {
            // Sem meter: se grid-limit ON usa política conservadora; senão corta (PV não pode atuar)
            if (grid_limit_enabled)
                apply_meter_timeout_policy_locked(idxs, active_cnt);
            else
                apply_budget_policy_locked(idxs, active_cnt, 0.0f);
        }
        else
        {
            if (pv_enabled && have_grid_snapshot)
            {
                // PV define budget total
                float budget_total_a = pv_optimizer_compute_budget_a(&grid_evt_snapshot,
                                                                     last_total_cmd_a,
                                                                     total_hw_max_a);

                apply_budget_policy_locked(idxs, active_cnt, budget_total_a);

                // Se também houver grid limit: aplica cap por import (A) usando watt_total (preferido)
                if (grid_limit_enabled)
                {
                    const float cur_total = sum_assigned_locked(idxs, active_cnt);
                    const float limited_total = grid_limiter_limit_total_a(&grid_evt_snapshot, grid_snapshot, cur_total);

                    if (limited_total + AVAILABLE_EPS < cur_total && cur_total > AVAILABLE_EPS)
                    {
                        float factor = limited_total / cur_total;
                        if (factor < 0.0f)
                            factor = 0.0f;
                        if (factor > 1.0f)
                            factor = 1.0f;

                        for (int k = 0; k < active_cnt; k++)
                        {
                            int i = idxs[k];
                            connectors[i].assigned *= factor;
                        }
                    }
                }
            }
            else
            {
                // Sem PV: se grid-limit ON usa a tua política atual; senão dá "max" (budget=hwmax)
                if (grid_limit_enabled)
                    apply_normal_policy_locked(idxs, active_cnt, grid_snapshot);
                else
                    apply_budget_policy_locked(idxs, active_cnt, total_hw_max_a);
            }
        }

        pending_cnt = prepare_pending_limits_locked(idxs, active_cnt, now, pending, CONNECTOR_COUNT);

        if (lb_mutex)
            xSemaphoreGive(lb_mutex);

        // 3) publicar limites
        if (pending_cnt > 0)
            post_pending_limits(pending, pending_cnt);

        vTaskDelay(pdMS_TO_TICKS(5000));
    }
}

void loadbalancer_init(void)
{
    pv_optimizer_init();
    grid_limiter_init();

    if (loadbalancer_load_config() != ESP_OK)
        ESP_LOGW(TAG, "Failed to load/init config. Using defaults.");

    lb_mutex = xSemaphoreCreateMutex();
    if (!lb_mutex)
        ESP_LOGE(TAG, "Failed to create loadbalancer mutex");

    init_connectors();

    input_filter_init(&grid_filter, 0.3f);
    input_filter_init(&grid_power_filter, 0.3f);

    if (xTaskCreate(loadbalancer_task, "loadbalancer", 8192, NULL, 4, NULL) != pdPASS)
        ESP_LOGE(TAG, "Failed to create loadbalancer task");

    loadbalancer_state_event_t evt = {.enabled = loadbalancer_enabled, .timestamp_us = esp_timer_get_time()};
    (void)esp_event_post(LOADBALANCER_EVENTS, LOADBALANCER_EVENT_INIT, &evt, sizeof(evt), portMAX_DELAY);

    ESP_ERROR_CHECK(esp_event_handler_register(METER_EVENT, METER_EVENT_DATA_READY,
                                               &loadbalancer_meter_event_handler, NULL));
    ESP_ERROR_CHECK(esp_event_handler_register(EVSE_EVENTS, EVSE_EVENT_STATE_CHANGED,
                                               &loadbalancer_evse_event_handler, NULL));
    ESP_ERROR_CHECK(esp_event_handler_register(EVSE_EVENTS, EVSE_EVENT_CONFIG_UPDATED,
                                               &on_evse_config_event, NULL));
    ESP_ERROR_CHECK(esp_event_handler_register(LOADBALANCER_EVENTS, LOADBALANCER_EVENT_SLAVE_STATUS,
                                               &on_slave_status, NULL));

    ESP_ERROR_CHECK(esp_event_handler_register(EVSE_LINK_EVENTS, LINK_EVENT_SLAVE_ONLINE,
                                               &on_evse_link_presence, NULL));
    ESP_ERROR_CHECK(esp_event_handler_register(EVSE_LINK_EVENTS, LINK_EVENT_SLAVE_OFFLINE,
                                               &on_evse_link_presence, NULL));
}
