/**
 * DreamStack ESP-NOW Transport — Implementation
 *
 * Handles ESP-NOW receive/send for binary signal frames,
 * and UDP listener for IR JSON push.
 */

#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "esp_wifi.h"
#include "esp_now.h"
#include "lwip/sockets.h"

#include "ds_espnow.h"

static const char *TAG = "ds-espnow";

// ─── State ───
static ds_espnow_config_t s_config;
static uint8_t s_seq = 0;
static int s_udp_sock = -1;
static TaskHandle_t s_udp_task = NULL;

// ─── IR fragment reassembly ───
#define MAX_IR_SIZE 16384
static uint8_t s_ir_buf[MAX_IR_SIZE];
static size_t  s_ir_len = 0;
static uint8_t s_frag_received = 0;
static uint8_t s_frag_total = 0;
static uint8_t s_frag_seq = 0xFF;

// ─── ESP-NOW Receive Callback ───
static void espnow_recv_cb(const esp_now_recv_info_t *recv_info,
                            const uint8_t *data, int len) {
    if (len < 1) return;

    uint8_t type = data[0];

    switch (type) {
    case DS_NOW_SIG:
        if (len >= sizeof(ds_sig_frame_t)) {
            const ds_sig_frame_t *f = (const ds_sig_frame_t *)data;
            if (s_config.on_signal) {
                s_config.on_signal(f->signal_id, f->value);
            }
        }
        break;

    case DS_NOW_SIG_BATCH:
        if (len >= sizeof(ds_sig_batch_t)) {
            const ds_sig_batch_t *b = (const ds_sig_batch_t *)data;
            const ds_sig_entry_t *entries = (const ds_sig_entry_t *)(data + sizeof(ds_sig_batch_t));
            size_t expected = sizeof(ds_sig_batch_t) + b->count * sizeof(ds_sig_entry_t);
            if (len >= expected && s_config.on_signal) {
                for (int i = 0; i < b->count; i++) {
                    s_config.on_signal(entries[i].id, entries[i].val);
                }
            }
        }
        break;

    case DS_NOW_PING: {
        // Respond with pong
        ds_heartbeat_t pong = { .type = DS_NOW_PONG, .seq = data[1] };
        esp_now_send(recv_info->src_addr, (const uint8_t *)&pong, sizeof(pong));
        break;
    }

    case DS_NOW_PONG:
        ESP_LOGD(TAG, "Pong received (seq=%d)", data[1]);
        break;

    default:
        ESP_LOGW(TAG, "Unknown ESP-NOW frame type: 0x%02x", type);
        break;
    }
}

// ─── ESP-NOW Send Callback ───
static void espnow_send_cb(const uint8_t *mac_addr, esp_now_send_status_t status) {
    if (status != ESP_NOW_SEND_SUCCESS) {
        ESP_LOGW(TAG, "ESP-NOW send failed");
    }
}

// ─── UDP Listener Task ───
// Receives IR JSON push and fragmented IR over UDP
static void udp_listener_task(void *arg) {
    struct sockaddr_in addr = {
        .sin_family = AF_INET,
        .sin_port = htons(DS_UDP_PORT),
        .sin_addr.s_addr = htonl(INADDR_ANY),
    };

    s_udp_sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
    if (s_udp_sock < 0) {
        ESP_LOGE(TAG, "Failed to create UDP socket");
        vTaskDelete(NULL);
        return;
    }

    if (bind(s_udp_sock, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
        ESP_LOGE(TAG, "Failed to bind UDP port %d", DS_UDP_PORT);
        close(s_udp_sock);
        s_udp_sock = -1;
        vTaskDelete(NULL);
        return;
    }

    ESP_LOGI(TAG, "UDP listener on port %d", DS_UDP_PORT);

    uint8_t buf[1500];
    while (1) {
        int len = recvfrom(s_udp_sock, buf, sizeof(buf), 0, NULL, NULL);
        if (len < 4) continue;

        // Check magic
        if (buf[0] != 0xD5 || buf[1] != 0x7A) continue;

        uint8_t frame_type = buf[2];

        if (frame_type == DS_UDP_IR_PUSH) {
            // Non-fragmented IR push
            const ds_ir_push_t *hdr = (const ds_ir_push_t *)buf;
            size_t json_len = hdr->length;
            if (json_len <= len - sizeof(ds_ir_push_t) && json_len < MAX_IR_SIZE) {
                memcpy(s_ir_buf, buf + sizeof(ds_ir_push_t), json_len);
                s_ir_buf[json_len] = '\0';
                ESP_LOGI(TAG, "IR push received (%zu bytes)", json_len);
                if (s_config.on_ir_push) {
                    s_config.on_ir_push((const char *)s_ir_buf, json_len);
                }
            }
        } else if (frame_type == DS_UDP_IR_FRAG) {
            // Fragmented IR push
            if (len < sizeof(ds_ir_frag_t)) continue;
            const ds_ir_frag_t *frag = (const ds_ir_frag_t *)buf;
            size_t payload_len = len - sizeof(ds_ir_frag_t);
            const uint8_t *payload = buf + sizeof(ds_ir_frag_t);

            // New fragment group?
            if (frag->seq != s_frag_seq) {
                s_frag_seq = frag->seq;
                s_frag_received = 0;
                s_frag_total = frag->frag_total;
                s_ir_len = 0;
            }

            // Append fragment (assume ordered delivery)
            if (s_ir_len + payload_len < MAX_IR_SIZE) {
                memcpy(s_ir_buf + s_ir_len, payload, payload_len);
                s_ir_len += payload_len;
                s_frag_received++;

                if (s_frag_received >= s_frag_total) {
                    s_ir_buf[s_ir_len] = '\0';
                    ESP_LOGI(TAG, "IR reassembled (%zu bytes, %d frags)",
                             s_ir_len, s_frag_total);
                    if (s_config.on_ir_push) {
                        s_config.on_ir_push((const char *)s_ir_buf, s_ir_len);
                    }
                }
            }
        }
    }
}

// ─── Public API ───

esp_err_t ds_espnow_init(const ds_espnow_config_t *config) {
    s_config = *config;
    if (s_config.channel == 0) s_config.channel = DS_ESPNOW_CHANNEL;

    // Initialize ESP-NOW
    esp_err_t ret = esp_now_init();
    if (ret != ESP_OK) {
        ESP_LOGE(TAG, "ESP-NOW init failed: %s", esp_err_to_name(ret));
        return ret;
    }

    esp_now_register_recv_cb(espnow_recv_cb);
    esp_now_register_send_cb(espnow_send_cb);

    // Add hub as peer
    esp_now_peer_info_t peer = {
        .channel = s_config.channel,
        .ifidx = WIFI_IF_STA,
        .encrypt = false,
    };
    memcpy(peer.peer_addr, s_config.hub_mac, 6);
    ret = esp_now_add_peer(&peer);
    if (ret != ESP_OK) {
        ESP_LOGW(TAG, "Add peer failed (may already exist): %s", esp_err_to_name(ret));
    }

    ESP_LOGI(TAG, "ESP-NOW initialized (ch=%d, hub=%02x:%02x:%02x:%02x:%02x:%02x)",
             s_config.channel,
             s_config.hub_mac[0], s_config.hub_mac[1], s_config.hub_mac[2],
             s_config.hub_mac[3], s_config.hub_mac[4], s_config.hub_mac[5]);

    // Start UDP listener
    xTaskCreate(udp_listener_task, "ds_udp", 4096, NULL, 5, &s_udp_task);

    return ESP_OK;
}

esp_err_t ds_espnow_send_action(uint8_t node_id, uint8_t action) {
    ds_action_frame_t frame = {
        .type = DS_NOW_ACTION,
        .node_id = node_id,
        .action = action,
        .seq = s_seq++,
    };
    return esp_now_send(s_config.hub_mac, (const uint8_t *)&frame, sizeof(frame));
}

esp_err_t ds_espnow_send_touch(uint8_t node_id, uint8_t event,
                                uint16_t x, uint16_t y) {
    ds_touch_now_t frame = {
        .type = DS_NOW_TOUCH,
        .node_id = node_id,
        .event = event,
        .seq = s_seq++,
        .x = x,
        .y = y,
    };
    return esp_now_send(s_config.hub_mac, (const uint8_t *)&frame, sizeof(frame));
}

esp_err_t ds_espnow_send_ping(void) {
    ds_heartbeat_t ping = { .type = DS_NOW_PING, .seq = s_seq++ };
    return esp_now_send(s_config.hub_mac, (const uint8_t *)&ping, sizeof(ping));
}

void ds_espnow_deinit(void) {
    if (s_udp_task) {
        vTaskDelete(s_udp_task);
        s_udp_task = NULL;
    }
    if (s_udp_sock >= 0) {
        close(s_udp_sock);
        s_udp_sock = -1;
    }
    esp_now_deinit();
    ESP_LOGI(TAG, "ESP-NOW deinitialized");
}
