#include "time_service.h"
#include "time_service_events.h"
#include "network_events.h"

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

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

#include <string.h>
#include <time.h>
#include <sys/time.h>
#include <stdlib.h>

static const char *TAG = "time_service";

/*
 * Portugal continental / Madeira / Canárias partilham esta regra POSIX em ESP-IDF.
 * Fonte da equivalência Europe/Lisbon -> POSIX: documentação Espressif RainMaker.
 */
#define TIME_SERVICE_TZ_NAME   "Europe/Lisbon"
#define TIME_SERVICE_TZ_POSIX  "WET0WEST,M3.5.0/1,M10.5.0"

#define TIME_SERVICE_SYNC_WAIT_MS   15000
#define TIME_SERVICE_MIN_VALID_YEAR 2024

typedef struct {
    bool timezone_configured;
    bool sntp_initialized;
    bool sync_in_progress;
    bool synced;
    bool inited;
} time_service_runtime_t;

static time_service_runtime_t s_rt = {
    .timezone_configured = false,
    .sntp_initialized = false,
    .sync_in_progress = false,
    .synced = false,
    .inited = false,
};

static TaskHandle_t s_sync_wait_task = NULL;

static void publish_state(time_service_event_id_t id)
{
    time_service_state_t st = {
        .timezone_configured = s_rt.timezone_configured,
        .sntp_initialized = s_rt.sntp_initialized,
        .sync_in_progress = s_rt.sync_in_progress,
        .synced = s_rt.synced,
        .valid_time = time_service_has_valid_time(),
        .now = time(NULL),
    };

    (void)esp_event_post(TIME_SERVICE_EVENTS, id, &st, sizeof(st), portMAX_DELAY);
}

static void set_portugal_timezone(void)
{
    setenv("TZ", TIME_SERVICE_TZ_POSIX, 1);
    tzset();
    s_rt.timezone_configured = true;
    ESP_LOGI(TAG, "Timezone set: %s (%s)", TIME_SERVICE_TZ_NAME, TIME_SERVICE_TZ_POSIX);
}

static void time_sync_notification_cb(struct timeval *tv)
{
    (void)tv;
    s_rt.synced = true;
    s_rt.sync_in_progress = false;

    time_t now = time(NULL);
    struct tm tm_now = {0};
    localtime_r(&now, &tm_now);

    ESP_LOGI(TAG, "Time synchronized: %04d-%02d-%02d %02d:%02d:%02d",
             tm_now.tm_year + 1900,
             tm_now.tm_mon + 1,
             tm_now.tm_mday,
             tm_now.tm_hour,
             tm_now.tm_min,
             tm_now.tm_sec);

    publish_state(TIME_SERVICE_EVENT_SYNC_OK);
}

static void ensure_sntp_initialized(void)
{
    if (s_rt.sntp_initialized)
        return;

    esp_sntp_config_t cfg = ESP_NETIF_SNTP_DEFAULT_CONFIG("pool.ntp.org");
    cfg.sync_cb = time_sync_notification_cb;

    esp_err_t err = esp_netif_sntp_init(&cfg);
    if (err == ESP_OK)
    {
        s_rt.sntp_initialized = true;
        ESP_LOGI(TAG, "SNTP initialized");
        return;
    }

    if (err == ESP_ERR_INVALID_STATE)
    {
        s_rt.sntp_initialized = true;
        ESP_LOGW(TAG, "SNTP already initialized elsewhere");
        return;
    }

    ESP_LOGE(TAG, "esp_netif_sntp_init failed: %s", esp_err_to_name(err));
}

static void sync_wait_task(void *arg)
{
    (void)arg;

    esp_err_t err = esp_netif_sntp_sync_wait(pdMS_TO_TICKS(TIME_SERVICE_SYNC_WAIT_MS));
    if (err == ESP_OK)
    {
        s_rt.synced = true;
        s_rt.sync_in_progress = false;
        ESP_LOGI(TAG, "Initial SNTP sync complete");
        publish_state(TIME_SERVICE_EVENT_SYNC_OK);
    }
    else
    {
        s_rt.sync_in_progress = false;
        ESP_LOGW(TAG, "SNTP sync wait timeout/error: %s", esp_err_to_name(err));
        publish_state(TIME_SERVICE_EVENT_SYNC_TIMEOUT);
    }

    s_sync_wait_task = NULL;
    vTaskDelete(NULL);
}

static esp_err_t start_sync_wait(void)
{
    if (!s_rt.sntp_initialized)
        return ESP_ERR_INVALID_STATE;

    if (s_rt.sync_in_progress)
        return ESP_ERR_INVALID_STATE;

    s_rt.sync_in_progress = true;
    publish_state(TIME_SERVICE_EVENT_SYNC_STARTED);

    BaseType_t ok = xTaskCreate(
        sync_wait_task,
        "time_sync_wait",
        4096,
        NULL,
        4,
        &s_sync_wait_task);

    if (ok != pdPASS)
    {
        s_rt.sync_in_progress = false;
        s_sync_wait_task = NULL;
        return ESP_FAIL;
    }

    return ESP_OK;
}

static void network_event_handler(void *arg, esp_event_base_t base, int32_t id, void *data)
{
    (void)arg;
    (void)base;
    (void)data;

    if (id == NETWORK_EVENT_STA_GOT_IP)
    {
        ESP_LOGI(TAG, "NETWORK_EVENT_STA_GOT_IP -> init/check SNTP");
        ensure_sntp_initialized();

        if (!s_rt.synced && !s_rt.sync_in_progress)
        {
            esp_err_t err = start_sync_wait();
            if (err != ESP_OK)
            {
                ESP_LOGW(TAG, "start_sync_wait failed: %s", esp_err_to_name(err));
            }
        }
    }
    else if (id == NETWORK_EVENT_STA_LOST_IP)
    {
        ESP_LOGW(TAG, "NETWORK_EVENT_STA_LOST_IP");
        publish_state(TIME_SERVICE_EVENT_NETWORK_LOST);
    }
}

void time_service_init(void)
{
    if (s_rt.inited)
    {
        ESP_LOGW(TAG, "time_service_init called twice");
        return;
    }

    set_portugal_timezone();

    ESP_ERROR_CHECK(esp_event_handler_register(
        NETWORK_EVENTS,
        NETWORK_EVENT_STA_GOT_IP,
        network_event_handler,
        NULL));

    ESP_ERROR_CHECK(esp_event_handler_register(
        NETWORK_EVENTS,
        NETWORK_EVENT_STA_LOST_IP,
        network_event_handler,
        NULL));

    s_rt.inited = true;
    publish_state(TIME_SERVICE_EVENT_INIT);
}

bool time_service_is_synced(void)
{
    return s_rt.synced;
}

bool time_service_is_sync_in_progress(void)
{
    return s_rt.sync_in_progress;
}

bool time_service_has_valid_time(void)
{
    time_t now = time(NULL);
    if (now <= 0)
        return false;

    struct tm tm_now = {0};
    localtime_r(&now, &tm_now);
    return (tm_now.tm_year + 1900) >= TIME_SERVICE_MIN_VALID_YEAR;
}

esp_err_t time_service_force_resync(void)
{
    if (!s_rt.sntp_initialized)
        return ESP_ERR_INVALID_STATE;

    if (s_rt.sync_in_progress)
        return ESP_ERR_INVALID_STATE;

    return start_sync_wait();
}

const char *time_service_get_tz_name(void)
{
    return TIME_SERVICE_TZ_NAME;
}

const char *time_service_get_tz_posix(void)
{
    return TIME_SERVICE_TZ_POSIX;
}

time_t time_service_get_epoch(void)
{
    return time(NULL);
}
