// components/scheduler/src/scheduler.c
#include "scheduler.h"
#include "scheduler_types.h"
#include "scheduler_events.h"

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

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

#include "storage_service.h"

#include <time.h>
#include <string.h>
#include <stdio.h>

static const char *TAG = "scheduler";

/* ===== Limites ===== */
#define SCHED_MIN_CURRENT_A 6
#define SCHED_MAX_CURRENT_A 80

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

/* ===== Estado interno ===== */
static sched_config_t s_cfg = {
    .enabled = false,
    .mode = SCHED_MODE_DISABLED,
    .window_count = 0,
};

static bool s_allowed_now = true;
static uint16_t s_current_limit_a = 0;

static TaskHandle_t s_task_handle = NULL;

/* ===== Storage ===== */
#define NVS_NAMESPACE "scheduler"
#define NVS_KEY_ENABLED "enabled" // u8
#define NVS_KEY_MODE "mode"       // u8
#define NVS_KEY_WCOUNT "wcount"   // u8

// por janela i: w0_s, w0_e, w0_a ...
static void make_wkey(char *out, size_t out_sz, int i, const char *suffix)
{
    if (!out || out_sz == 0)
        return;
    snprintf(out, out_sz, "w%d_%s", i, suffix);
}

static uint16_t clamp_u16(uint16_t v, uint16_t lo, uint16_t hi)
{
    if (v < lo)
        return lo;
    if (v > hi)
        return hi;
    return v;
}

static bool cfg_sanitize(sched_config_t *cfg)
{
    if (!cfg)
        return false;
    bool changed = false;

    cfg->enabled = cfg->enabled ? true : false;

    if (cfg->mode > SCHED_MODE_DAILY_MULTI)
    {
        cfg->mode = SCHED_MODE_DISABLED;
        changed = true;
    }

    if (cfg->window_count > SCHED_MAX_WINDOWS)
    {
        cfg->window_count = SCHED_MAX_WINDOWS;
        changed = true;
    }

    for (int i = 0; i < cfg->window_count && i < SCHED_MAX_WINDOWS; i++)
    {
        sched_window_t *w = &cfg->windows[i];

        if (w->start_min > 1440)
        {
            w->start_min = 0;
            changed = true;
        }
        if (w->end_min > 1440)
        {
            w->end_min = 1440;
            changed = true;
        }

        uint16_t olda = w->current_a;
        w->current_a = clamp_u16(w->current_a, SCHED_MIN_CURRENT_A, SCHED_MAX_CURRENT_A);
        if (w->current_a != olda)
            changed = true;
    }

    return changed;
}

static void load_config_from_storage(sched_config_t *out)
{
    if (!out)
        return;

    *out = s_cfg;
    bool needs_flush = false;

    uint8_t u8 = 0;
    uint16_t u16 = 0;
    esp_err_t err;

    // enabled
    err = storage_get_u8_sync(NVS_NAMESPACE, NVS_KEY_ENABLED, &u8, TO_TICKS_MS(800));
    if (err == ESP_OK && u8 <= 1)
        out->enabled = (u8 != 0);
    else
    {
        out->enabled = false;
        (void)storage_set_u8_async(NVS_NAMESPACE, NVS_KEY_ENABLED, 0);
        needs_flush = true;
        ESP_LOGW(TAG, "Missing/invalid enabled (%s) -> default=false (persisted)",
                 esp_err_to_name(err));
    }

    // mode
    err = storage_get_u8_sync(NVS_NAMESPACE, NVS_KEY_MODE, &u8, TO_TICKS_MS(800));
    if (err == ESP_OK && u8 <= (uint8_t)SCHED_MODE_DAILY_MULTI)
        out->mode = (sched_mode_t)u8;
    else
    {
        out->mode = SCHED_MODE_DISABLED;
        (void)storage_set_u8_async(NVS_NAMESPACE, NVS_KEY_MODE, (uint8_t)out->mode);
        needs_flush = true;
        ESP_LOGW(TAG, "Missing/invalid mode (%s) -> default=DISABLED (persisted)",
                 esp_err_to_name(err));
    }

    // wcount
    err = storage_get_u8_sync(NVS_NAMESPACE, NVS_KEY_WCOUNT, &u8, TO_TICKS_MS(800));
    if (err == ESP_OK && u8 <= SCHED_MAX_WINDOWS)
        out->window_count = u8;
    else
    {
        out->window_count = 0;
        (void)storage_set_u8_async(NVS_NAMESPACE, NVS_KEY_WCOUNT, 0);
        needs_flush = true;
        ESP_LOGW(TAG, "Missing/invalid wcount (%s) -> default=0 (persisted)",
                 esp_err_to_name(err));
    }

    // windows
    for (int i = 0; i < out->window_count && i < SCHED_MAX_WINDOWS; i++)
    {
        char key[16];

        make_wkey(key, sizeof(key), i, "s");
        err = storage_get_u16_sync(NVS_NAMESPACE, key, &u16, TO_TICKS_MS(800));
        if (err == ESP_OK && u16 <= 1440)
            out->windows[i].start_min = u16;
        else
        {
            out->windows[i].start_min = 0;
            (void)storage_set_u16_async(NVS_NAMESPACE, key, 0);
            needs_flush = true;
        }

        make_wkey(key, sizeof(key), i, "e");
        err = storage_get_u16_sync(NVS_NAMESPACE, key, &u16, TO_TICKS_MS(800));
        if (err == ESP_OK && u16 <= 1440)
            out->windows[i].end_min = u16;
        else
        {
            out->windows[i].end_min = 1440;
            (void)storage_set_u16_async(NVS_NAMESPACE, key, 1440);
            needs_flush = true;
        }

        make_wkey(key, sizeof(key), i, "a");
        err = storage_get_u16_sync(NVS_NAMESPACE, key, &u16, TO_TICKS_MS(800));
        if (err == ESP_OK && u16 >= SCHED_MIN_CURRENT_A && u16 <= SCHED_MAX_CURRENT_A)
            out->windows[i].current_a = u16;
        else
        {
            out->windows[i].current_a = 16;
            (void)storage_set_u16_async(NVS_NAMESPACE, key, 16);
            needs_flush = true;
        }
    }

    if (cfg_sanitize(out))
    {
        (void)storage_set_u8_async(NVS_NAMESPACE, NVS_KEY_ENABLED, out->enabled ? 1 : 0);
        (void)storage_set_u8_async(NVS_NAMESPACE, NVS_KEY_MODE, (uint8_t)out->mode);
        (void)storage_set_u8_async(NVS_NAMESPACE, NVS_KEY_WCOUNT, out->window_count);

        for (int i = 0; i < out->window_count && i < SCHED_MAX_WINDOWS; i++)
        {
            char key[16];
            make_wkey(key, sizeof(key), i, "s");
            (void)storage_set_u16_async(NVS_NAMESPACE, key, out->windows[i].start_min);
            make_wkey(key, sizeof(key), i, "e");
            (void)storage_set_u16_async(NVS_NAMESPACE, key, out->windows[i].end_min);
            make_wkey(key, sizeof(key), i, "a");
            (void)storage_set_u16_async(NVS_NAMESPACE, key, out->windows[i].current_a);
        }

        needs_flush = true;
    }

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

    ESP_LOGI(TAG, "Loaded: enabled=%d mode=%s wcount=%u",
             (int)out->enabled, sched_mode_to_str(out->mode), (unsigned)out->window_count);
}

static void save_config_to_storage(const sched_config_t *cfg)
{
    if (!cfg)
        return;

    (void)storage_set_u8_async(NVS_NAMESPACE, NVS_KEY_ENABLED, cfg->enabled ? 1 : 0);
    (void)storage_set_u8_async(NVS_NAMESPACE, NVS_KEY_MODE, (uint8_t)cfg->mode);
    (void)storage_set_u8_async(NVS_NAMESPACE, NVS_KEY_WCOUNT, cfg->window_count);

    for (int i = 0; i < cfg->window_count && i < SCHED_MAX_WINDOWS; i++)
    {
        char key[16];
        make_wkey(key, sizeof(key), i, "s");
        (void)storage_set_u16_async(NVS_NAMESPACE, key, cfg->windows[i].start_min);
        make_wkey(key, sizeof(key), i, "e");
        (void)storage_set_u16_async(NVS_NAMESPACE, key, cfg->windows[i].end_min);
        make_wkey(key, sizeof(key), i, "a");
        (void)storage_set_u16_async(NVS_NAMESPACE, key, cfg->windows[i].current_a);
    }
}

static bool in_window(uint16_t start, uint16_t end, uint16_t now_min)
{
    if (start <= end)
        return (now_min >= start && now_min < end);
    return (now_min >= start || now_min < end); // cruza meia-noite
}

static void compute_allowed_and_current_now(const sched_config_t *cfg, bool *allowed, uint16_t *curr_a)
{
    if (!cfg || !allowed || !curr_a)
        return;

    if (!cfg->enabled || cfg->mode == SCHED_MODE_DISABLED)
    {
        *allowed = true; // scheduler desligado => não bloqueia
        *curr_a = 0;
        return;
    }

    // enabled + DAILY_MULTI: fora de todas as janelas => bloqueia (opção 1)
    time_t now = time(NULL);
    struct tm tm_now = {0};
    localtime_r(&now, &tm_now);
    uint16_t now_min = (uint16_t)(tm_now.tm_hour * 60 + tm_now.tm_min);

    if (cfg->window_count == 0)
    {
        *allowed = false;
        *curr_a = 0;
        return;
    }

    bool any = false;
    uint16_t best = 0xFFFF; // overlaps => min por segurança

    for (int i = 0; i < cfg->window_count && i < SCHED_MAX_WINDOWS; i++)
    {
        const sched_window_t *w = &cfg->windows[i];
        if (in_window(w->start_min, w->end_min, now_min))
        {
            any = true;
            if (w->current_a < best)
                best = w->current_a;
        }
    }

    *allowed = any;
    *curr_a = any ? best : 0;
}

static void scheduler_recompute_and_emit(void)
{
    bool new_allowed = true;
    uint16_t new_curr = 0;

    compute_allowed_and_current_now(&s_cfg, &new_allowed, &new_curr);

    if (new_allowed != s_allowed_now || new_curr != s_current_limit_a)
    {
        s_allowed_now = new_allowed;
        s_current_limit_a = new_curr;

        ESP_LOGI(TAG, "allowed_now=%d current_limit=%uA",
                 (int)s_allowed_now, (unsigned)s_current_limit_a);

        sched_event_state_t ev = {
            .allowed_now = s_allowed_now,
            .current_limit_a = s_current_limit_a};

        (void)esp_event_post(SCHED_EVENTS, SCHED_EVENT_WINDOW_CHANGED,
                             &ev, sizeof(ev), portMAX_DELAY);
    }
}

static void scheduler_task(void *arg)
{
    (void)arg;
    const TickType_t period = pdMS_TO_TICKS(60000);

    ESP_LOGI(TAG, "Scheduler task started");
    for (;;)
    {
        scheduler_recompute_and_emit();
        vTaskDelay(period);
    }
}

void scheduler_init(void)
{
    esp_err_t se = storage_service_init();
    if (se != ESP_OK)
    {
        ESP_LOGE(TAG, "storage_service_init failed: %s (using defaults in RAM)", esp_err_to_name(se));
    }
    else
    {
        load_config_from_storage(&s_cfg);
    }

    compute_allowed_and_current_now(&s_cfg, &s_allowed_now, &s_current_limit_a);

    sched_event_state_t ev = {
        .allowed_now = s_allowed_now,
        .current_limit_a = s_current_limit_a};
    (void)esp_event_post(SCHED_EVENTS, SCHED_EVENT_INIT,
                         &ev, sizeof(ev), portMAX_DELAY);

    ESP_LOGI(TAG, "Init: allowed_now=%d current_limit=%uA",
             (int)s_allowed_now, (unsigned)s_current_limit_a);

    if (s_task_handle == NULL)
    {
        (void)xTaskCreate(scheduler_task, "scheduler_task", 4 * 1024,
                          NULL, 3, &s_task_handle);
    }
}

void scheduler_set_config(const sched_config_t *cfg)
{
    if (!cfg)
        return;

    s_cfg = *cfg;
    (void)cfg_sanitize(&s_cfg);

    save_config_to_storage(&s_cfg);
    scheduler_recompute_and_emit();
}

sched_config_t scheduler_get_config(void)
{
    return s_cfg;
}

bool scheduler_is_allowed_now(void)
{
    return s_allowed_now;
}

uint16_t scheduler_get_current_limit_now_a(void)
{
    return s_current_limit_a;
}