#include "ws_client.h"
#include "esp_websocket_client.h"
#include "esp_log.h"
#include "cJSON.h"
#include <string.h>
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <freertos/timers.h>

static const char *TAG = "ws_client";

#define WS_WATCHDOG_PERIOD_MS   5000
#define WS_DATA_TIMEOUT_MS      10000

static esp_websocket_client_handle_t s_client = NULL;
static pi_stats_t s_stats = {};
static SemaphoreHandle_t s_stats_mutex = NULL;
static ws_state_t s_state = WS_STATE_DISCONNECTED;
static ws_data_callback_t s_data_cb = NULL;
static ws_state_callback_t s_state_cb = NULL;
static TickType_t s_last_data_tick = 0;
static TimerHandle_t s_watchdog_timer = NULL;

static void watchdog_callback(TimerHandle_t timer)
{
    (void)timer;
    if (s_state == WS_STATE_CONNECTED &&
        (xTaskGetTickCount() - s_last_data_tick) > pdMS_TO_TICKS(WS_DATA_TIMEOUT_MS)) {
        ESP_LOGW(TAG, "WS watchdog: no data for %ds, forcing reconnect",
                 WS_DATA_TIMEOUT_MS / 1000);
        esp_websocket_client_close(s_client, pdMS_TO_TICKS(2000));
    }
}

static void set_state(ws_state_t state)
{
    s_state = state;
    if (s_state_cb) {
        s_state_cb(state);
    }
}

static void parse_stats_json(const char *data, int len)
{
    cJSON *root = cJSON_ParseWithLength(data, len);
    if (!root) {
        ESP_LOGW(TAG, "JSON parse failed");
        return;
    }

    if (xSemaphoreTake(s_stats_mutex, pdMS_TO_TICKS(100)) == pdTRUE) {
        cJSON *item;

        item = cJSON_GetObjectItem(root, "cpu_pct");
        if (item) s_stats.cpu_pct = (float)item->valuedouble;

        item = cJSON_GetObjectItem(root, "mem_pct");
        if (item) s_stats.mem_pct = (float)item->valuedouble;

        item = cJSON_GetObjectItem(root, "mem_used_mb");
        if (item) s_stats.mem_used_mb = (uint32_t)item->valuedouble;

        item = cJSON_GetObjectItem(root, "disk_pct");
        if (item) s_stats.disk_pct = (float)item->valuedouble;

        item = cJSON_GetObjectItem(root, "cpu_temp");
        if (item) s_stats.cpu_temp = (float)item->valuedouble;

        item = cJSON_GetObjectItem(root, "uptime_hrs");
        if (item) s_stats.uptime_hrs = (float)item->valuedouble;

        item = cJSON_GetObjectItem(root, "net_rx_kbps");
        if (item) s_stats.net_rx_kbps = (float)item->valuedouble;

        item = cJSON_GetObjectItem(root, "net_tx_kbps");
        if (item) s_stats.net_tx_kbps = (float)item->valuedouble;

        item = cJSON_GetObjectItem(root, "timestamp");
        if (item) s_stats.last_update = (uint32_t)item->valuedouble;

        cJSON *services = cJSON_GetObjectItem(root, "services");
        if (cJSON_IsArray(services)) {
            int count = cJSON_GetArraySize(services);
            if (count > WS_MAX_SERVICES) count = WS_MAX_SERVICES;
            s_stats.service_count = count;
            for (int i = 0; i < count; i++) {
                cJSON *svc = cJSON_GetArrayItem(services, i);
                cJSON *name = cJSON_GetObjectItem(svc, "name");
                cJSON *status = cJSON_GetObjectItem(svc, "status");
                if (name && name->valuestring) {
                    strncpy(s_stats.services[i].name, name->valuestring, WS_SERVICE_NAME_LEN - 1);
                    s_stats.services[i].name[WS_SERVICE_NAME_LEN - 1] = '\0';
                }
                if (status && status->valuestring) {
                    if (strcmp(status->valuestring, "running") == 0) {
                        s_stats.services[i].status = SVC_RUNNING;
                    } else if (strcmp(status->valuestring, "warning") == 0) {
                        s_stats.services[i].status = SVC_WARNING;
                    } else {
                        s_stats.services[i].status = SVC_STOPPED;
                    }
                }
            }
        }

        /* Parse local_time object for RTC sync */
        cJSON *lt = cJSON_GetObjectItem(root, "local_time");
        if (cJSON_IsObject(lt)) {
            cJSON *y  = cJSON_GetObjectItem(lt, "y");
            cJSON *mo = cJSON_GetObjectItem(lt, "mo");
            cJSON *d  = cJSON_GetObjectItem(lt, "d");
            cJSON *h  = cJSON_GetObjectItem(lt, "h");
            cJSON *m  = cJSON_GetObjectItem(lt, "m");
            cJSON *s  = cJSON_GetObjectItem(lt, "s");
            if (y && mo && d && h && m && s) {
                s_stats.time_year   = (uint16_t)y->valuedouble;
                s_stats.time_month  = (uint8_t)mo->valuedouble;
                s_stats.time_day    = (uint8_t)d->valuedouble;
                s_stats.time_hour   = (uint8_t)h->valuedouble;
                s_stats.time_minute = (uint8_t)m->valuedouble;
                s_stats.time_second = (uint8_t)s->valuedouble;
                s_stats.time_valid  = true;
            } else {
                s_stats.time_valid = false;
            }
        } else {
            s_stats.time_valid = false;
        }

        s_stats.valid = true;
        xSemaphoreGive(s_stats_mutex);

        s_last_data_tick = xTaskGetTickCount();

        if (s_data_cb) {
            s_data_cb(&s_stats);
        }
    }

    cJSON_Delete(root);
}

static void ws_event_handler(void *arg, esp_event_base_t event_base,
                             int32_t event_id, void *event_data)
{
    esp_websocket_event_data_t *data = (esp_websocket_event_data_t *)event_data;

    switch (event_id) {
    case WEBSOCKET_EVENT_CONNECTED:
        ESP_LOGI(TAG, "WebSocket connected");
        s_last_data_tick = xTaskGetTickCount();
        set_state(WS_STATE_CONNECTED);
        break;
    case WEBSOCKET_EVENT_DISCONNECTED:
        ESP_LOGW(TAG, "WebSocket disconnected");
        set_state(WS_STATE_DISCONNECTED);
        break;
    case WEBSOCKET_EVENT_DATA:
        if (data->op_code == 0x01 && data->data_len > 0) { // text frame
            ESP_LOGD(TAG, "Received %d bytes", data->data_len);
            parse_stats_json(data->data_ptr, data->data_len);
        }
        break;
    case WEBSOCKET_EVENT_ERROR:
        ESP_LOGE(TAG, "WebSocket error");
        set_state(WS_STATE_ERROR);
        break;
    default:
        break;
    }
}

void ws_client_init(const char *uri)
{
    s_stats_mutex = xSemaphoreCreateMutex();

    esp_websocket_client_config_t config = {};
    config.uri = uri;
    config.reconnect_timeout_ms = 5000;
    config.buffer_size = 2048;

    s_client = esp_websocket_client_init(&config);
    esp_websocket_register_events(s_client, WEBSOCKET_EVENT_ANY, ws_event_handler, NULL);

    s_watchdog_timer = xTimerCreate("ws_wd", pdMS_TO_TICKS(WS_WATCHDOG_PERIOD_MS),
                                     pdTRUE, NULL, watchdog_callback);

    ESP_LOGI(TAG, "WS client initialized: %s", uri);
}

void ws_client_start(void)
{
    if (s_client) {
        set_state(WS_STATE_CONNECTING);
        esp_websocket_client_start(s_client);
        if (s_watchdog_timer) {
            xTimerStart(s_watchdog_timer, 0);
        }
    }
}

void ws_client_stop(void)
{
    if (s_client) {
        if (s_watchdog_timer) {
            xTimerStop(s_watchdog_timer, 0);
        }
        esp_websocket_client_stop(s_client);
        set_state(WS_STATE_DISCONNECTED);
    }
}

void ws_client_reconnect(void)
{
    if (!s_client) return;
    ESP_LOGI(TAG, "Manual WS reconnect triggered");
    esp_websocket_client_close(s_client, pdMS_TO_TICKS(2000));
    s_last_data_tick = xTaskGetTickCount();
}

ws_state_t ws_client_get_state(void)
{
    return s_state;
}

void ws_client_get_stats(pi_stats_t *out)
{
    if (xSemaphoreTake(s_stats_mutex, pdMS_TO_TICKS(100)) == pdTRUE) {
        memcpy(out, &s_stats, sizeof(pi_stats_t));
        xSemaphoreGive(s_stats_mutex);
    }
}

void ws_client_set_data_callback(ws_data_callback_t cb)
{
    s_data_cb = cb;
}

void ws_client_set_state_callback(ws_state_callback_t cb)
{
    s_state_cb = cb;
}
