/*
 * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
 *
 * SPDX-License-Identifier: Apache-2.0
 */
/* Get recv router csi

   This example code is in the Public Domain (or CC0 licensed, at your option.)

   Unless required by applicable law or agreed to in writing, this
   software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
   CONDITIONS OF ANY KIND, either express or implied.
*/
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <math.h>
#include <errno.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/event_groups.h"

#include "nvs_flash.h"
#include "nvs.h"

#include "esp_mac.h"
#include "rom/ets_sys.h"
#include "esp_log.h"
#include "esp_wifi.h"
#include "esp_netif.h"
#include "esp_now.h"
#include "esp_timer.h"
#include "esp_task_wdt.h"
#include "esp_pm.h"
#include "esp_heap_caps.h"
#include "esp_random.h"
#include "esp_ota_ops.h"
#include "esp_https_ota.h"
#include "esp_partition.h"
#include "esp_chip_info.h"
#include "esp_http_client.h"
#include "driver/gpio.h"
#include "soc/soc_caps.h"
#if SOC_TEMP_SENSOR_SUPPORTED
#include "driver/temperature_sensor.h"
#endif
#include "mdns.h"
#include "mbedtls/md.h"

#include "lwip/inet.h"
#include "lwip/netdb.h"
#include "lwip/sockets.h"
#include "ping/ping_sock.h"

#include "nimble/nimble_port.h"
#include "nimble/nimble_port_freertos.h"
#include "host/ble_hs.h"
#include "host/util/util.h"
#include "protocol_examples_common.h"
#include "esp_csi_gain_ctrl.h"

#define CONFIG_SEND_FREQUENCY_DEFAULT  100
#if CONFIG_IDF_TARGET_ESP32C5 || CONFIG_IDF_TARGET_ESP32C61
#define CSI_FORCE_LLTF                      0
#endif
#define CONFIG_FORCE_GAIN                   0

#if CONFIG_IDF_TARGET_ESP32S3 || CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32C5 || CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32C61
#define CONFIG_GAIN_CONTROL                 1
#endif

#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
#define ESP_IF_WIFI_STA ESP_MAC_WIFI_STA
#endif

#define LED_GPIO  GPIO_NUM_2

static const char *TAG = "csi_recv_router";

/* --- LED modes --- */
typedef enum {
    LED_OFF,
    LED_SLOW_BLINK,
    LED_FAST_BLINK,
    LED_SOLID,
    LED_OTA,
} led_mode_t;

/* --- Globals --- */
static int s_send_frequency = CONFIG_SEND_FREQUENCY_DEFAULT;
static int8_t s_tx_power_dbm = 10;
static esp_ping_handle_t s_ping_handle = NULL;
static volatile led_mode_t s_led_mode = LED_OFF;
static bool s_led_quiet = true;  /* quiet mode: off normally, solid on motion/presence */
static volatile int64_t s_last_csi_time = 0;
static volatile int64_t s_identify_end_time = 0;
static volatile bool s_ota_in_progress = false;
static volatile uint32_t s_csi_count = 0;
static volatile bool s_wifi_connected = false;
static volatile int8_t s_rssi_min = 0;
static volatile int8_t s_rssi_max = -128;
static uint32_t s_boot_count = 0;

/* CSI collection toggle */
static bool s_csi_enabled = true;

/* CSI output mode */
typedef enum {
    CSI_MODE_RAW     = 0,
    CSI_MODE_COMPACT = 1,
    CSI_MODE_HYBRID  = 2,
} csi_mode_t;

typedef struct {
    float amp_rms;
    float amp_std;
    float amp_max;
    uint8_t amp_max_idx;
    uint8_t n_sub;
    uint32_t energy;
} csi_features_t;

static csi_mode_t s_csi_mode = CSI_MODE_RAW;
static int s_hybrid_interval = 10;

/* Adaptive sampling */
#define WANDER_WINDOW       50
#define RATE_ACTIVE         100
#define RATE_IDLE           10
#define IDLE_HOLDOFF_US     3000000LL  /* 3s of no motion before dropping rate */
#define DEFAULT_THRESHOLD   0.002f

static bool s_adaptive = false;
static float s_motion_threshold = DEFAULT_THRESHOLD;
static volatile bool s_motion_detected = false;
static volatile int64_t s_last_motion_time = 0;
static uint32_t s_energy_buf[WANDER_WINDOW];
static uint32_t s_energy_idx = 0;

/* BLE scanning */
#define BLE_SCAN_RESTART_DEFAULT_US  30000000LL  /* restart scan every 30s to refresh duplicate filter */
static int64_t s_ble_scan_interval_us = BLE_SCAN_RESTART_DEFAULT_US;
static bool s_ble_enabled = false;
static uint8_t s_ble_own_addr_type;
static esp_timer_handle_t s_ble_timer = NULL;

/* Chip temperature sensor */
#if SOC_TEMP_SENSOR_SUPPORTED
static temperature_sensor_handle_t s_temp_handle = NULL;
#endif

/* UDP socket for CSI data transmission */
static int s_udp_socket = -1;
static struct sockaddr_in s_dest_addr;
static char s_udp_buffer[2048];
static char s_target_ip[16];   /* runtime target IP (NVS or Kconfig default) */
static uint16_t s_target_port; /* runtime target port */
static char s_hostname[32];    /* runtime hostname (NVS or Kconfig default) */
static char s_auth_secret[65] = "";  /* empty = auth disabled */

/* Deauth flood detection */
#define FLOOD_WINDOW_DEFAULT  10
#define FLOOD_THRESH_DEFAULT  5
#define FLOOD_RING_SIZE       64

static int s_flood_thresh = FLOOD_THRESH_DEFAULT;
static int s_flood_window_s = FLOOD_WINDOW_DEFAULT;
static bool s_flood_active = false;
static int64_t s_flood_alert_ts = 0;
static struct { int64_t ts; } s_deauth_ring[FLOOD_RING_SIZE];
static int s_deauth_ring_head = 0;
static int s_deauth_ring_count = 0;

/* Power test */
static volatile bool s_powertest_running = false;
static bool s_powersave = false;

/* Baseline calibration & presence detection */
#define BASELINE_MAX_SUBS   64
#define PRESENCE_WINDOW     50
#define PRESENCE_HOLDOFF_US 10000000LL  /* 10s debounce */
#define DEFAULT_PR_THRESH   0.05f       /* 5% normalized distance */

static float s_baseline_amps[BASELINE_MAX_SUBS];
static int   s_baseline_nsub = 0;          /* 0 = no baseline */
static bool  s_presence_enabled = false;
static float s_pr_threshold = DEFAULT_PR_THRESH;
static volatile float s_pr_last_score = 0.0f;
static volatile bool  s_presence_detected = false;
static int64_t s_last_presence_time = 0;

/* Calibration state (transient) */
static volatile bool     s_calibrating = false;
static float    s_calib_accum[BASELINE_MAX_SUBS];
static volatile uint32_t s_calib_count = 0;
static int      s_calib_target = 0;
static int      s_calib_nsub = 0;

/* Presence scoring buffer */
static float    s_pr_scores[PRESENCE_WINDOW];
static uint32_t s_pr_score_idx = 0;

/* Multi-channel scanning */
#define CHANSCAN_CHANNELS   13
#define CHANSCAN_DWELL_MS   100
static bool     s_chanscan_enabled = false;
static int      s_chanscan_interval_s = 300;  /* 5 min default */
static volatile bool s_chanscan_active = false;
static int64_t  s_chanscan_last = 0;

/* Probe dedup rate (moved before config_load_nvs for NVS access) */
#define PROBE_DEDUP_DEFAULT_US  10000000LL
static int64_t s_probe_dedup_us = PROBE_DEDUP_DEFAULT_US;

/* Alert thresholds (0 = disabled) */
#define ALERT_HOLDOFF_US  60000000LL  /* 60s debounce between alerts */
static float    s_alert_temp_thresh = 0.0f;  /* celsius, e.g. 70.0 */
static uint32_t s_alert_heap_thresh = 0;     /* bytes, e.g. 30000 */
#if SOC_TEMP_SENSOR_SUPPORTED
static int64_t  s_alert_temp_last = 0;
#endif
static int64_t  s_alert_heap_last = 0;

/* --- NVS helpers --- */

static esp_err_t config_save_str(const char *key, const char *value);

static void config_load_nvs(void)
{
    /* Start with Kconfig defaults */
    strncpy(s_hostname, CONFIG_CSI_HOSTNAME, sizeof(s_hostname) - 1);
    s_hostname[sizeof(s_hostname) - 1] = '\0';
    strncpy(s_target_ip, CONFIG_CSI_UDP_TARGET_IP, sizeof(s_target_ip) - 1);
    s_target_ip[sizeof(s_target_ip) - 1] = '\0';
    s_target_port = CONFIG_CSI_UDP_TARGET_PORT;

    nvs_handle_t h;
    if (nvs_open("csi_config", NVS_READONLY, &h) == ESP_OK) {
        int32_t val;
        if (nvs_get_i32(h, "send_rate", &val) == ESP_OK && val >= 10 && val <= 100) {
            s_send_frequency = (int)val;
        }
        int8_t pwr;
        if (nvs_get_i8(h, "tx_power", &pwr) == ESP_OK && pwr >= 2 && pwr <= 20) {
            s_tx_power_dbm = pwr;
        }
        int8_t adaptive;
        if (nvs_get_i8(h, "adaptive", &adaptive) == ESP_OK) {
            s_adaptive = (adaptive != 0);
        }
        int32_t thresh;
        if (nvs_get_i32(h, "threshold", &thresh) == ESP_OK && thresh > 0) {
            s_motion_threshold = (float)thresh / 1000000.0f;
        }
        int8_t ble;
        if (nvs_get_i8(h, "ble_scan", &ble) == ESP_OK) {
            s_ble_enabled = (ble != 0);
        }
        size_t ip_len = sizeof(s_target_ip);
        nvs_get_str(h, "target_ip", s_target_ip, &ip_len);
        int32_t port;
        if (nvs_get_i32(h, "target_port", &port) == ESP_OK && port > 0 && port <= 65535) {
            s_target_port = (uint16_t)port;
        }
        size_t hn_len = sizeof(s_hostname);
        nvs_get_str(h, "hostname", s_hostname, &hn_len);
        int8_t csi_mode;
        if (nvs_get_i8(h, "csi_mode", &csi_mode) == ESP_OK && csi_mode >= 0 && csi_mode <= 2) {
            s_csi_mode = (csi_mode_t)csi_mode;
        }
        int32_t hybrid_n;
        if (nvs_get_i32(h, "hybrid_n", &hybrid_n) == ESP_OK && hybrid_n >= 1 && hybrid_n <= 100) {
            s_hybrid_interval = (int)hybrid_n;
        }
        size_t sec_len = sizeof(s_auth_secret);
        nvs_get_str(h, "auth_secret", s_auth_secret, &sec_len);
        int32_t flood_t;
        if (nvs_get_i32(h, "flood_thresh", &flood_t) == ESP_OK && flood_t >= 1 && flood_t <= 100) {
            s_flood_thresh = (int)flood_t;
        }
        int32_t flood_w;
        if (nvs_get_i32(h, "flood_window", &flood_w) == ESP_OK && flood_w >= 1 && flood_w <= 300) {
            s_flood_window_s = (int)flood_w;
        }
        int32_t scan_r;
        if (nvs_get_i32(h, "scan_rate", &scan_r) == ESP_OK && scan_r >= 5 && scan_r <= 300) {
            s_ble_scan_interval_us = (int64_t)scan_r * 1000000LL;
        }
        int32_t probe_r;
        if (nvs_get_i32(h, "probe_rate", &probe_r) == ESP_OK && probe_r >= 1 && probe_r <= 300) {
            s_probe_dedup_us = (int64_t)probe_r * 1000000LL;
        }
        int8_t powersave;
        if (nvs_get_i8(h, "powersave", &powersave) == ESP_OK) {
            s_powersave = (powersave != 0);
        }
        int8_t presence;
        if (nvs_get_i8(h, "presence", &presence) == ESP_OK) {
            s_presence_enabled = (presence != 0);
        }
        int32_t pr_thresh;
        if (nvs_get_i32(h, "pr_thresh", &pr_thresh) == ESP_OK && pr_thresh > 0) {
            s_pr_threshold = (float)pr_thresh / 1000000.0f;
        }
        int8_t bl_nsub;
        if (nvs_get_i8(h, "bl_nsub", &bl_nsub) == ESP_OK && bl_nsub > 0 && bl_nsub <= BASELINE_MAX_SUBS) {
            size_t bl_len = (size_t)bl_nsub * sizeof(float);
            if (nvs_get_blob(h, "bl_amps", s_baseline_amps, &bl_len) == ESP_OK) {
                s_baseline_nsub = (int)bl_nsub;
            }
        }
        int8_t chanscan;
        if (nvs_get_i8(h, "chanscan", &chanscan) == ESP_OK) {
            s_chanscan_enabled = (chanscan != 0);
        }
        int32_t chanscan_int;
        if (nvs_get_i32(h, "chanscan_int", &chanscan_int) == ESP_OK && chanscan_int >= 60 && chanscan_int <= 3600) {
            s_chanscan_interval_s = (int)chanscan_int;
        }
        int8_t led_quiet;
        if (nvs_get_i8(h, "led_quiet", &led_quiet) == ESP_OK) {
            s_led_quiet = (led_quiet != 0);
        }
        int8_t csi_en;
        if (nvs_get_i8(h, "csi_enabled", &csi_en) == ESP_OK) {
            s_csi_enabled = (csi_en != 0);
        }
        int32_t alert_temp;
        if (nvs_get_i32(h, "alert_temp", &alert_temp) == ESP_OK && alert_temp > 0) {
            s_alert_temp_thresh = (float)alert_temp / 10.0f;  /* stored as 10ths of degree */
        }
        int32_t alert_heap;
        if (nvs_get_i32(h, "alert_heap", &alert_heap) == ESP_OK && alert_heap > 0) {
            s_alert_heap_thresh = (uint32_t)alert_heap;
        }
        nvs_close(h);
        ESP_LOGI(TAG, "NVS loaded: hostname=%s rate=%d tx_power=%d adaptive=%d threshold=%.6f ble=%d target=%s:%d csi_mode=%d hybrid_n=%d powersave=%d presence=%d pr_thresh=%.4f baseline_nsub=%d led_quiet=%d csi=%d alert_temp=%.1f alert_heap=%lu",
                 s_hostname, s_send_frequency, s_tx_power_dbm, s_adaptive, s_motion_threshold, s_ble_enabled,
                 s_target_ip, s_target_port, (int)s_csi_mode, s_hybrid_interval, s_powersave,
                 s_presence_enabled, s_pr_threshold, s_baseline_nsub, s_led_quiet, s_csi_enabled,
                 s_alert_temp_thresh, (unsigned long)s_alert_heap_thresh);
    } else {
        ESP_LOGI(TAG, "NVS: no saved config, using defaults");
    }

    /* Auto-generate auth secret on first boot */
    if (s_auth_secret[0] == '\0') {
        uint8_t rand_bytes[16];
        esp_fill_random(rand_bytes, sizeof(rand_bytes));
        for (int i = 0; i < 16; i++) {
            snprintf(s_auth_secret + i * 2, 3, "%02x", rand_bytes[i]);
        }
        s_auth_secret[32] = '\0';
        config_save_str("auth_secret", s_auth_secret);
        ESP_LOGW(TAG, "AUTH: secret generated (%.4s... — retrieve via serial or NVS)", s_auth_secret);
    }

    /* Boot counter — always increment, even on first boot */
    nvs_handle_t bh;
    if (nvs_open("csi_config", NVS_READWRITE, &bh) == ESP_OK) {
        int32_t bc = 0;
        nvs_get_i32(bh, "boot_count", &bc);
        s_boot_count = (uint32_t)(bc + 1);
        nvs_set_i32(bh, "boot_count", (int32_t)s_boot_count);
        nvs_commit(bh);
        nvs_close(bh);
        ESP_LOGI(TAG, "Boot count: %lu", (unsigned long)s_boot_count);
    }
}

/* NVS write throttle: max 20 writes per 10s to prevent flash wear attacks */
#define NVS_WRITES_MAX     20
#define NVS_WINDOW_US      10000000LL

static int64_t s_nvs_window_start = 0;
static int     s_nvs_window_writes = 0;

static bool nvs_write_throttle(void)
{
    int64_t now = esp_timer_get_time();
    if (now - s_nvs_window_start > NVS_WINDOW_US) {
        s_nvs_window_start = now;
        s_nvs_window_writes = 0;
    }
    if (s_nvs_window_writes >= NVS_WRITES_MAX) {
        ESP_LOGW(TAG, "NVS write throttled");
        return false;
    }
    s_nvs_window_writes++;
    return true;
}

static esp_err_t config_save_i32(const char *key, int32_t value)
{
    if (!nvs_write_throttle()) return ESP_ERR_INVALID_STATE;
    nvs_handle_t h;
    esp_err_t err = nvs_open("csi_config", NVS_READWRITE, &h);
    if (err != ESP_OK) return err;
    err = nvs_set_i32(h, key, value);
    if (err == ESP_OK) err = nvs_commit(h);
    nvs_close(h);
    return err;
}

static esp_err_t config_save_i8(const char *key, int8_t value)
{
    if (!nvs_write_throttle()) return ESP_ERR_INVALID_STATE;
    nvs_handle_t h;
    esp_err_t err = nvs_open("csi_config", NVS_READWRITE, &h);
    if (err != ESP_OK) return err;
    err = nvs_set_i8(h, key, value);
    if (err == ESP_OK) err = nvs_commit(h);
    nvs_close(h);
    return err;
}

static esp_err_t config_save_str(const char *key, const char *value)
{
    if (!nvs_write_throttle()) return ESP_ERR_INVALID_STATE;
    nvs_handle_t h;
    esp_err_t err = nvs_open("csi_config", NVS_READWRITE, &h);
    if (err != ESP_OK) return err;
    err = nvs_set_str(h, key, value);
    if (err == ESP_OK) err = nvs_commit(h);
    nvs_close(h);
    return err;
}

static esp_err_t config_save_blob(const char *key, const void *data, size_t len)
{
    if (!nvs_write_throttle()) return ESP_ERR_INVALID_STATE;
    nvs_handle_t h;
    esp_err_t err = nvs_open("csi_config", NVS_READWRITE, &h);
    if (err != ESP_OK) return err;
    err = nvs_set_blob(h, key, data, len);
    if (err == ESP_OK) err = nvs_commit(h);
    nvs_close(h);
    return err;
}

static esp_err_t config_erase_key(const char *key)
{
    nvs_handle_t h;
    esp_err_t err = nvs_open("csi_config", NVS_READWRITE, &h);
    if (err != ESP_OK) return err;
    err = nvs_erase_key(h, key);
    if (err == ESP_OK) err = nvs_commit(h);
    nvs_close(h);
    return err;
}

/* --- LED --- */

static void led_gpio_init(void)
{
    gpio_config_t io_conf = {
        .pin_bit_mask = (1ULL << LED_GPIO),
        .mode = GPIO_MODE_OUTPUT,
        .pull_up_en = GPIO_PULLUP_DISABLE,
        .pull_down_en = GPIO_PULLDOWN_DISABLE,
        .intr_type = GPIO_INTR_DISABLE,
    };
    gpio_config(&io_conf);
    gpio_set_level(LED_GPIO, 0);
}

static void led_task(void *arg)
{
    bool led_on = false;
    while (1) {
        /* Check identify timeout */
        if (s_led_mode == LED_SOLID && s_identify_end_time > 0) {
            if (esp_timer_get_time() >= s_identify_end_time) {
                s_identify_end_time = 0;
                s_led_mode = s_led_quiet ? LED_OFF : LED_SLOW_BLINK;
            }
        }

        /* Quiet mode: off normally, solid on motion/presence, OTA pattern during OTA */
        if (s_led_quiet && s_led_mode != LED_OTA && s_identify_end_time == 0) {
            bool activity = s_motion_detected ||
                           (s_presence_enabled && s_baseline_nsub > 0 && s_pr_last_score > s_pr_threshold);
            if (activity) {
                gpio_set_level(LED_GPIO, 1);
                led_on = true;
            } else {
                gpio_set_level(LED_GPIO, 0);
                led_on = false;
            }
            vTaskDelay(pdMS_TO_TICKS(200));
            continue;
        }

        /* Auto-switch between slow/fast blink based on CSI activity */
        if (s_led_mode == LED_SLOW_BLINK || s_led_mode == LED_FAST_BLINK) {
            int64_t now = esp_timer_get_time();
            if (s_last_csi_time > 0 && (now - s_last_csi_time) < 500000) {
                s_led_mode = LED_FAST_BLINK;
            } else if (s_led_mode == LED_FAST_BLINK) {
                s_led_mode = LED_SLOW_BLINK;
            }
        }

        switch (s_led_mode) {
        case LED_OFF:
            gpio_set_level(LED_GPIO, 0);
            led_on = false;
            vTaskDelay(pdMS_TO_TICKS(200));
            break;
        case LED_SLOW_BLINK:
            led_on = !led_on;
            gpio_set_level(LED_GPIO, led_on ? 1 : 0);
            vTaskDelay(pdMS_TO_TICKS(500));
            break;
        case LED_FAST_BLINK:
            led_on = !led_on;
            gpio_set_level(LED_GPIO, led_on ? 1 : 0);
            vTaskDelay(pdMS_TO_TICKS(100));
            break;
        case LED_SOLID:
            gpio_set_level(LED_GPIO, 1);
            led_on = true;
            vTaskDelay(pdMS_TO_TICKS(200));
            break;
        case LED_OTA:
            /* Double-blink: on-off-on-off-pause */
            gpio_set_level(LED_GPIO, 1);
            vTaskDelay(pdMS_TO_TICKS(80));
            gpio_set_level(LED_GPIO, 0);
            vTaskDelay(pdMS_TO_TICKS(80));
            gpio_set_level(LED_GPIO, 1);
            vTaskDelay(pdMS_TO_TICKS(80));
            gpio_set_level(LED_GPIO, 0);
            vTaskDelay(pdMS_TO_TICKS(500));
            led_on = false;
            break;
        }
    }
}

/* --- CSI feature extraction --- */

static void csi_extract_features(const int8_t *buf, int len, float gain, csi_features_t *out, float *amps_out)
{
    int n_pairs = len / 2;
    if (n_pairs > 64) n_pairs = 64;

    float amps[64];
    float sum_sq = 0.0f;
    float sum_amp = 0.0f;
    float max_amp = 0.0f;
    uint8_t max_idx = 0;
    uint32_t energy = 0;

    /* Pass 1: compute per-subcarrier amplitude, accumulate sums */
    for (int i = 0; i < n_pairs; i++) {
        float iv = gain * buf[i * 2];
        float qv = gain * buf[i * 2 + 1];
        float amp = sqrtf(iv * iv + qv * qv);
        amps[i] = amp;
        sum_amp += amp;
        sum_sq += iv * iv + qv * qv;
        energy += abs(buf[i * 2]) + abs(buf[i * 2 + 1]);
        if (amp > max_amp) {
            max_amp = amp;
            max_idx = (uint8_t)i;
        }
    }

    if (amps_out && n_pairs > 0) {
        memcpy(amps_out, amps, n_pairs * sizeof(float));
    }

    float mean_amp = (n_pairs > 0) ? sum_amp / n_pairs : 0.0f;

    /* Pass 2: compute variance */
    float var = 0.0f;
    for (int i = 0; i < n_pairs; i++) {
        float d = amps[i] - mean_amp;
        var += d * d;
    }
    var = (n_pairs > 0) ? var / n_pairs : 0.0f;

    out->amp_rms = (n_pairs > 0) ? sqrtf(sum_sq / n_pairs) : 0.0f;
    out->amp_std = sqrtf(var);
    out->amp_max = max_amp;
    out->amp_max_idx = max_idx;
    out->n_sub = (uint8_t)n_pairs;
    out->energy = energy;
}

/* --- CSI callback --- */

static void wifi_csi_rx_cb(void *ctx, wifi_csi_info_t *info)
{
    if (!info || !info->buf) {
        ESP_LOGW(TAG, "<%s> wifi_csi_cb", esp_err_to_name(ESP_ERR_INVALID_ARG));
        return;
    }

    if (memcmp(info->mac, ctx, 6)) {
        return;
    }

    if (!s_csi_enabled) {
        return;
    }

    s_last_csi_time = esp_timer_get_time();

    const wifi_pkt_rx_ctrl_t *rx_ctrl = &info->rx_ctrl;

    /* Track RSSI min/max */
    int8_t rssi = rx_ctrl->rssi;
    if (s_csi_count == 0 || rssi < s_rssi_min) s_rssi_min = rssi;
    if (s_csi_count == 0 || rssi > s_rssi_max) s_rssi_max = rssi;
    float compensate_gain = 1.0f;
    static uint8_t agc_gain = 0;
    static int8_t fft_gain = 0;
#if CONFIG_GAIN_CONTROL
    static uint8_t agc_gain_baseline = 0;
    static int8_t fft_gain_baseline = 0;
    esp_csi_gain_ctrl_get_rx_gain(rx_ctrl, &agc_gain, &fft_gain);
    if (s_csi_count < 100) {
        esp_csi_gain_ctrl_record_rx_gain(agc_gain, fft_gain);
    } else if (s_csi_count == 100) {
        esp_csi_gain_ctrl_get_rx_gain_baseline(&agc_gain_baseline, &fft_gain_baseline);
#if CONFIG_FORCE_GAIN
        esp_csi_gain_ctrl_set_rx_force_gain(agc_gain_baseline, fft_gain_baseline);
        ESP_LOGI(TAG, "fft_force %d, agc_force %d", fft_gain_baseline, agc_gain_baseline);
#endif
    }
    esp_csi_gain_ctrl_get_gain_compensation(&compensate_gain, agc_gain, fft_gain);
    ESP_LOGD(TAG, "compensate_gain %f, agc_gain %d, fft_gain %d", compensate_gain, agc_gain, fft_gain);
#endif

    /* Extract features (used for compact mode, adaptive sampling, calibration, presence) */
    csi_features_t features = {0};
    bool need_amps = s_calibrating || (s_presence_enabled && s_baseline_nsub > 0);
    static float s_live_amps[64];
    if (s_csi_mode != CSI_MODE_RAW || s_adaptive || need_amps) {
        csi_extract_features(info->buf, info->len, compensate_gain, &features,
                             need_amps ? s_live_amps : NULL);
    }

    /* Calibration: accumulate per-subcarrier amplitudes */
    if (s_calibrating && features.n_sub > 0) {
        if (s_calib_count == 0) {
            s_calib_nsub = features.n_sub;
            memset(s_calib_accum, 0, sizeof(s_calib_accum));
        }
        int nsub = (features.n_sub < s_calib_nsub) ? features.n_sub : s_calib_nsub;
        for (int i = 0; i < nsub; i++) {
            s_calib_accum[i] += s_live_amps[i];
        }
        s_calib_count++;
    }

    /* Presence scoring: normalized Euclidean distance vs baseline */
    if (!s_calibrating && s_presence_enabled && s_baseline_nsub > 0 && features.n_sub > 0) {
        int nsub = (features.n_sub < s_baseline_nsub) ? features.n_sub : s_baseline_nsub;
        float sum_diff_sq = 0.0f;
        float sum_base_sq = 0.0f;
        for (int i = 0; i < nsub; i++) {
            float d = s_live_amps[i] - s_baseline_amps[i];
            sum_diff_sq += d * d;
            sum_base_sq += s_baseline_amps[i] * s_baseline_amps[i];
        }
        float score = (sum_base_sq > 0.0f) ? sqrtf(sum_diff_sq / sum_base_sq) : 0.0f;
        s_pr_scores[s_pr_score_idx % PRESENCE_WINDOW] = score;
        s_pr_score_idx++;
    }

    /* Determine whether to send raw I/Q data this packet */
    csi_mode_t mode = s_csi_mode;
    bool send_raw = (mode == CSI_MODE_RAW) ||
                    (mode == CSI_MODE_HYBRID && (s_csi_count % s_hybrid_interval) == 0);

    /* Build CSI data into buffer for UDP transmission */
    int pos = 0;

#if CONFIG_IDF_TARGET_ESP32C5 || CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32C61
    if (!s_csi_count) {
        ESP_LOGI(TAG, "================ CSI RECV (UDP) ================");
    }
    pos = snprintf(s_udp_buffer, sizeof(s_udp_buffer),
               "CSI_DATA,%s,%lu," MACSTR ",%d,%d,%d,%d,%d,%d,%d,%d,%d",
               s_hostname, (unsigned long)s_csi_count, MAC2STR(info->mac), rx_ctrl->rssi, rx_ctrl->rate,
               rx_ctrl->noise_floor, fft_gain, agc_gain, rx_ctrl->channel,
               rx_ctrl->timestamp, rx_ctrl->sig_len, rx_ctrl->rx_state);
#else
    if (!s_csi_count) {
        ESP_LOGI(TAG, "================ CSI RECV (UDP) ================");
    }
    pos = snprintf(s_udp_buffer, sizeof(s_udp_buffer),
               "CSI_DATA,%s,%lu," MACSTR ",%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d",
               s_hostname, (unsigned long)s_csi_count, MAC2STR(info->mac), rx_ctrl->rssi, rx_ctrl->rate, rx_ctrl->sig_mode,
               rx_ctrl->mcs, rx_ctrl->cwb, rx_ctrl->smoothing, rx_ctrl->not_sounding,
               rx_ctrl->aggregation, rx_ctrl->stbc, rx_ctrl->fec_coding, rx_ctrl->sgi,
               rx_ctrl->noise_floor, rx_ctrl->ampdu_cnt, rx_ctrl->channel, rx_ctrl->secondary_channel,
               rx_ctrl->timestamp, rx_ctrl->ant, rx_ctrl->sig_len, rx_ctrl->rx_state);
#endif

    if (send_raw) {
        /* Raw I/Q array payload */
#if (CONFIG_IDF_TARGET_ESP32C5 || CONFIG_IDF_TARGET_ESP32C61) && CSI_FORCE_LLTF
        int16_t csi = ((int16_t)(((((uint16_t)info->buf[1]) << 8) | info->buf[0]) << 4) >> 4);
        pos += snprintf(s_udp_buffer + pos, sizeof(s_udp_buffer) - pos,
                        ",%d,%d,\"[%d", (info->len - 2) / 2, info->first_word_invalid, (int16_t)(compensate_gain * csi));
        for (int i = 2; i < (info->len - 2); i += 2) {
            csi = ((int16_t)(((((uint16_t)info->buf[i + 1]) << 8) | info->buf[i]) << 4) >> 4);
            pos += snprintf(s_udp_buffer + pos, sizeof(s_udp_buffer) - pos, ",%d", (int16_t)(compensate_gain * csi));
        }
#else
        pos += snprintf(s_udp_buffer + pos, sizeof(s_udp_buffer) - pos,
                        ",%d,%d,\"[%d", info->len, info->first_word_invalid, (int16_t)(compensate_gain * info->buf[0]));
        for (int i = 1; i < info->len; i++) {
            pos += snprintf(s_udp_buffer + pos, sizeof(s_udp_buffer) - pos, ",%d", (int16_t)(compensate_gain * info->buf[i]));
        }
#endif
        pos += snprintf(s_udp_buffer + pos, sizeof(s_udp_buffer) - pos, "]\"\n");
    } else {
        /* Compact feature payload */
        pos += snprintf(s_udp_buffer + pos, sizeof(s_udp_buffer) - pos,
                        ",%d,%d,\"F:%.1f,%.1f,%.1f,%u,%lu\"\n",
                        info->len, info->first_word_invalid,
                        features.amp_rms, features.amp_std, features.amp_max,
                        (unsigned)features.amp_max_idx, (unsigned long)features.energy);
    }

    /* Send via UDP */
    if (s_udp_socket >= 0) {
        sendto(s_udp_socket, s_udp_buffer, pos, 0, (struct sockaddr *)&s_dest_addr, sizeof(s_dest_addr));
    }

    /* Adaptive sampling: reuse extracted energy (features always computed when adaptive is on) */
    if (s_adaptive) {
        s_energy_buf[s_energy_idx % WANDER_WINDOW] = features.energy;
        s_energy_idx++;
    }

    s_csi_count++;
}

static void wifi_csi_init()
{
    /**
     * @brief In order to ensure the compatibility of routers, only LLTF sub-carriers are selected.
     */
#if CONFIG_IDF_TARGET_ESP32C5 || CONFIG_IDF_TARGET_ESP32C61
    wifi_csi_config_t csi_config = {
        .enable                   = true,
        .acquire_csi_legacy       = true,
        .acquire_csi_force_lltf   = CSI_FORCE_LLTF,
        .acquire_csi_ht20         = true,
        .acquire_csi_ht40         = true,
        .acquire_csi_vht          = false,
        .acquire_csi_su           = false,
        .acquire_csi_mu           = false,
        .acquire_csi_dcm          = false,
        .acquire_csi_beamformed   = false,
        .acquire_csi_he_stbc_mode = 2,
        .val_scale_cfg            = 0,
        .dump_ack_en              = false,
        .reserved                 = false
    };
#elif CONFIG_IDF_TARGET_ESP32C6
    wifi_csi_config_t csi_config = {
        .enable                 = true,
        .acquire_csi_legacy     = true,
        .acquire_csi_ht20       = true,
        .acquire_csi_ht40       = true,
        .acquire_csi_su         = false,
        .acquire_csi_mu         = false,
        .acquire_csi_dcm        = false,
        .acquire_csi_beamformed = false,
        .acquire_csi_he_stbc    = 2,
        .val_scale_cfg          = false,
        .dump_ack_en            = false,
        .reserved               = false
    };
#else
    wifi_csi_config_t csi_config = {
        .lltf_en           = true,
        .htltf_en          = false,
        .stbc_htltf2_en    = false,
        .ltf_merge_en      = true,
        .channel_filter_en = true,
        .manu_scale        = true,
        .shift             = true,
    };
#endif
    static wifi_ap_record_t s_ap_info = {0};
    ESP_ERROR_CHECK(esp_wifi_sta_get_ap_info(&s_ap_info));
    ESP_ERROR_CHECK(esp_wifi_set_csi_config(&csi_config));
    ESP_ERROR_CHECK(esp_wifi_set_csi_rx_cb(wifi_csi_rx_cb, s_ap_info.bssid));
    ESP_ERROR_CHECK(esp_wifi_set_csi(true));
}

static void udp_socket_init(void)
{
    s_udp_socket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
    if (s_udp_socket < 0) {
        ESP_LOGE(TAG, "Failed to create UDP socket: errno %d", errno);
        return;
    }

    memset(&s_dest_addr, 0, sizeof(s_dest_addr));
    s_dest_addr.sin_family = AF_INET;
    s_dest_addr.sin_port = htons(s_target_port);
    inet_pton(AF_INET, s_target_ip, &s_dest_addr.sin_addr);

    ESP_LOGI(TAG, "UDP socket initialized, sending to %s:%d",
             s_target_ip, s_target_port);
}

/* --- Ping --- */

static esp_err_t wifi_ping_router_start(void)
{
    /* Stop existing session if any */
    if (s_ping_handle) {
        esp_ping_stop(s_ping_handle);
        esp_ping_delete_session(s_ping_handle);
        s_ping_handle = NULL;
    }

    esp_ping_config_t ping_config = ESP_PING_DEFAULT_CONFIG();
    ping_config.count             = 0;
    ping_config.interval_ms       = 1000 / s_send_frequency;
    ping_config.task_stack_size   = 3072;
    ping_config.data_size         = 1;

    esp_netif_ip_info_t local_ip;
    esp_netif_get_ip_info(esp_netif_get_handle_from_ifkey("WIFI_STA_DEF"), &local_ip);
    ESP_LOGI(TAG, "got ip:" IPSTR ", gw: " IPSTR, IP2STR(&local_ip.ip), IP2STR(&local_ip.gw));
    ping_config.target_addr.u_addr.ip4.addr = ip4_addr_get_u32(&local_ip.gw);
    ping_config.target_addr.type = ESP_IPADDR_TYPE_V4;

    esp_ping_callbacks_t cbs = { 0 };
    esp_ping_new_session(&ping_config, &cbs, &s_ping_handle);
    esp_ping_start(s_ping_handle);

    ESP_LOGI(TAG, "Ping started at %d Hz", s_send_frequency);
    return ESP_OK;
}

/* --- BLE scanning --- */

static int ble_gap_event_cb(struct ble_gap_event *event, void *arg);

static void ble_scan_start(void)
{
    struct ble_gap_disc_params disc_params = {0};
    disc_params.passive = 1;
    disc_params.filter_duplicates = 1;
    disc_params.itvl = 0;
    disc_params.window = 0;
    disc_params.filter_policy = 0;
    disc_params.limited = 0;

    int rc = ble_gap_disc(s_ble_own_addr_type, BLE_HS_FOREVER, &disc_params,
                          ble_gap_event_cb, NULL);
    if (rc != 0) {
        ESP_LOGE(TAG, "BLE: scan start failed rc=%d", rc);
    } else {
        ESP_LOGI(TAG, "BLE: scan started ok");
    }
}

static int ble_gap_event_cb(struct ble_gap_event *event, void *arg)
{
    switch (event->type) {
    case BLE_GAP_EVENT_DISC: {
        struct ble_gap_disc_desc *disc = &event->disc;
        ESP_LOGI(TAG, "BLE: disc event rssi=%d addr=%02x:%02x:%02x:%02x:%02x:%02x",
                 disc->rssi, disc->addr.val[5], disc->addr.val[4], disc->addr.val[3],
                 disc->addr.val[2], disc->addr.val[1], disc->addr.val[0]);

        /* Parse advertisement for device name, manufacturer data, TX power, flags */
        struct ble_hs_adv_fields fields;
        int rc = ble_hs_adv_parse_fields(&fields, disc->data, disc->length_data);

        char name[32] = "";
        uint16_t company_id = 0;
        int8_t tx_power = 127;  /* 127 = not present */
        uint8_t adv_flags = 0;

        if (rc == 0) {
            /* Device name */
            if (fields.name != NULL && fields.name_len > 0) {
                int nlen = fields.name_len < (int)sizeof(name) - 1 ? fields.name_len : (int)sizeof(name) - 1;
                memcpy(name, fields.name, nlen);
                name[nlen] = '\0';
            }

            /* Manufacturer-specific data: first 2 bytes = company ID (little-endian) */
            if (fields.mfg_data != NULL && fields.mfg_data_len >= 2) {
                company_id = fields.mfg_data[0] | (fields.mfg_data[1] << 8);
            }

            /* TX power level */
            if (fields.tx_pwr_lvl_is_present) {
                tx_power = fields.tx_pwr_lvl;
            }

            /* Advertisement flags (always present in struct, 0 if not in advert) */
            adv_flags = fields.flags;
        }

        /* Send BLE_DATA via UDP with extended format */
        char buf[192];
        int len = snprintf(buf, sizeof(buf),
            "BLE_DATA,%s,%02x:%02x:%02x:%02x:%02x:%02x,%d,%s,%s,0x%04X,%d,%u\n",
            s_hostname,
            disc->addr.val[5], disc->addr.val[4], disc->addr.val[3],
            disc->addr.val[2], disc->addr.val[1], disc->addr.val[0],
            disc->rssi,
            disc->addr.type == BLE_ADDR_PUBLIC ? "pub" : "rnd",
            name,
            company_id,
            (int)tx_power,
            (unsigned)adv_flags);

        if (s_udp_socket >= 0) {
            sendto(s_udp_socket, buf, len, 0,
                (struct sockaddr *)&s_dest_addr, sizeof(s_dest_addr));
        }
        break;
    }
    case BLE_GAP_EVENT_DISC_COMPLETE:
        if (s_ble_enabled) {
            ble_scan_start();
        }
        break;
    default:
        break;
    }
    return 0;
}

static void ble_scan_restart_timer_cb(void *arg)
{
    if (s_ble_enabled) {
        ble_gap_disc_cancel();
        ble_scan_start();
    }
}

static void ble_on_sync(void)
{
    int rc = ble_hs_util_ensure_addr(0);
    if (rc != 0) {
        ESP_LOGE(TAG, "BLE: ensure addr failed rc=%d", rc);
        return;
    }
    rc = ble_hs_id_infer_auto(0, &s_ble_own_addr_type);
    if (rc != 0) {
        ESP_LOGE(TAG, "BLE: infer addr type failed rc=%d", rc);
        return;
    }
    ESP_LOGI(TAG, "BLE: stack synced, addr_type=%d", s_ble_own_addr_type);

    if (s_ble_enabled) {
        ble_scan_start();
    }
}

static void ble_on_reset(int reason)
{
    ESP_LOGW(TAG, "BLE: stack reset reason=%d", reason);
}

static void ble_host_task(void *param)
{
    ESP_LOGI(TAG, "BLE host task started");
    nimble_port_run();
    nimble_port_freertos_deinit();
}

/* --- Forward declarations --- */
static void channel_scan_run(void);

/* --- Adaptive sampling --- */

static void adaptive_task(void *arg)
{
    while (1) {
        vTaskDelay(pdMS_TO_TICKS(500));

        /* Calibration finalization (NVS I/O happens here, not in WiFi callback) */
        if (s_calibrating && s_calib_count >= (uint32_t)s_calib_target) {
            int nsub = s_calib_nsub;
            uint32_t count = s_calib_count;
            for (int i = 0; i < nsub; i++) {
                s_baseline_amps[i] = s_calib_accum[i] / (float)count;
            }
            s_baseline_nsub = nsub;
            config_save_blob("bl_amps", s_baseline_amps, nsub * sizeof(float));
            config_save_i8("bl_nsub", (int8_t)nsub);

            char event[128];
            int len = snprintf(event, sizeof(event),
                "EVENT,%s,calibrate=done packets=%lu nsub=%d",
                s_hostname, (unsigned long)count, nsub);
            if (s_udp_socket >= 0) {
                sendto(s_udp_socket, event, len, 0,
                    (struct sockaddr *)&s_dest_addr, sizeof(s_dest_addr));
            }
            ESP_LOGI(TAG, "Calibration done: %lu packets, %d subcarriers",
                     (unsigned long)count, nsub);
            s_calibrating = false;
        }

        /* Presence event emission */
        if (s_presence_enabled && s_baseline_nsub > 0 && s_pr_score_idx >= PRESENCE_WINDOW) {
            float sum = 0.0f;
            for (int i = 0; i < PRESENCE_WINDOW; i++) {
                sum += s_pr_scores[i];
            }
            float mean_score = sum / PRESENCE_WINDOW;
            s_pr_last_score = mean_score;

            bool detected = (mean_score > s_pr_threshold);
            if (detected != s_presence_detected) {
                int64_t now = esp_timer_get_time();
                if (now - s_last_presence_time > PRESENCE_HOLDOFF_US) {
                    s_presence_detected = detected;
                    s_last_presence_time = now;

                    char event[128];
                    int len = snprintf(event, sizeof(event),
                        "EVENT,%s,presence=%d score=%.4f",
                        s_hostname, detected ? 1 : 0, mean_score);
                    if (s_udp_socket >= 0) {
                        sendto(s_udp_socket, event, len, 0,
                            (struct sockaddr *)&s_dest_addr, sizeof(s_dest_addr));
                    }
                    ESP_LOGI(TAG, "Presence: %s (score=%.4f threshold=%.4f)",
                             detected ? "detected" : "cleared", mean_score, s_pr_threshold);
                }
            }
        }

        /* Periodic channel scanning */
        if (s_chanscan_enabled && !s_chanscan_active) {
            int64_t now = esp_timer_get_time();
            int64_t interval_us = (int64_t)s_chanscan_interval_s * 1000000LL;
            if (s_chanscan_last == 0 || (now - s_chanscan_last) >= interval_us) {
                channel_scan_run();
            }
        }

        /* Alert threshold checks */
        {
            int64_t now_alert = esp_timer_get_time();

            /* Heap alert */
            if (s_alert_heap_thresh > 0) {
                uint32_t free_heap = esp_get_free_heap_size();
                if (free_heap < s_alert_heap_thresh &&
                    (now_alert - s_alert_heap_last) > ALERT_HOLDOFF_US) {
                    s_alert_heap_last = now_alert;
                    char event[128];
                    int len = snprintf(event, sizeof(event),
                        "EVENT,%s,alert=heap free=%lu thresh=%lu",
                        s_hostname, (unsigned long)free_heap,
                        (unsigned long)s_alert_heap_thresh);
                    if (s_udp_socket >= 0) {
                        sendto(s_udp_socket, event, len, 0,
                            (struct sockaddr *)&s_dest_addr, sizeof(s_dest_addr));
                    }
                    ESP_LOGW(TAG, "ALERT: heap low (%lu < %lu)",
                             (unsigned long)free_heap, (unsigned long)s_alert_heap_thresh);
                }
            }

            /* Temperature alert */
#if SOC_TEMP_SENSOR_SUPPORTED
            if (s_alert_temp_thresh > 0.0f && s_temp_handle) {
                float temp = 0.0f;
                if (temperature_sensor_get_celsius(s_temp_handle, &temp) == ESP_OK &&
                    temp > s_alert_temp_thresh &&
                    (now_alert - s_alert_temp_last) > ALERT_HOLDOFF_US) {
                    s_alert_temp_last = now_alert;
                    char event[128];
                    int len = snprintf(event, sizeof(event),
                        "EVENT,%s,alert=temp value=%.1f thresh=%.1f",
                        s_hostname, temp, s_alert_temp_thresh);
                    if (s_udp_socket >= 0) {
                        sendto(s_udp_socket, event, len, 0,
                            (struct sockaddr *)&s_dest_addr, sizeof(s_dest_addr));
                    }
                    ESP_LOGW(TAG, "ALERT: temp high (%.1f > %.1f)",
                             temp, s_alert_temp_thresh);
                }
            }
#endif
        }

        if (!s_adaptive || s_energy_idx < WANDER_WINDOW) continue;

        /* Compute mean */
        float mean = 0;
        for (int i = 0; i < WANDER_WINDOW; i++) {
            mean += s_energy_buf[i];
        }
        mean /= WANDER_WINDOW;

        if (mean < 1.0f) continue;

        /* Compute variance */
        float var = 0;
        for (int i = 0; i < WANDER_WINDOW; i++) {
            float d = s_energy_buf[i] - mean;
            var += d * d;
        }
        var /= WANDER_WINDOW;

        /* Wander = coefficient of variation squared */
        float wander = var / (mean * mean);

        int64_t now = esp_timer_get_time();
        bool motion = wander > s_motion_threshold;

        if (motion) {
            s_last_motion_time = now;
        }

        int target_rate;
        if (motion || (now - s_last_motion_time < IDLE_HOLDOFF_US)) {
            target_rate = RATE_ACTIVE;
        } else {
            target_rate = RATE_IDLE;
        }

        s_motion_detected = motion;

        if (target_rate != s_send_frequency) {
            s_send_frequency = target_rate;
            wifi_ping_router_start();

            /* Notify Pi */
            char event[128];
            int len = snprintf(event, sizeof(event),
                "EVENT,%s,motion=%d rate=%d wander=%.6f",
                s_hostname, motion ? 1 : 0, target_rate, wander);
            if (s_udp_socket >= 0) {
                sendto(s_udp_socket, event, len, 0,
                    (struct sockaddr *)&s_dest_addr, sizeof(s_dest_addr));
            }

            ESP_LOGI(TAG, "Adaptive: %s -> %d Hz (wander=%.6f)",
                motion ? "motion" : "idle", target_rate, wander);
        }
    }
}

/* --- Channel scanning --- */

static void channel_scan_run(void)
{
    if (s_chanscan_active) return;
    s_chanscan_active = true;

    /* Get current AP info for return */
    wifi_ap_record_t ap;
    uint8_t home_channel = 1;
    if (esp_wifi_sta_get_ap_info(&ap) == ESP_OK) {
        home_channel = ap.primary;
    }

    ESP_LOGI(TAG, "CHANSCAN: starting scan, home channel=%d", home_channel);

    /* Stop ping to pause CSI during scan */
    if (s_ping_handle) {
        esp_ping_stop(s_ping_handle);
        esp_ping_delete_session(s_ping_handle);
        s_ping_handle = NULL;
    }

    /* Hop through channels 1-13 */
    for (int ch = 1; ch <= CHANSCAN_CHANNELS; ch++) {
        esp_wifi_set_channel(ch, WIFI_SECOND_CHAN_NONE);
        vTaskDelay(pdMS_TO_TICKS(CHANSCAN_DWELL_MS));
    }

    /* Return to AP channel */
    esp_wifi_set_channel(home_channel, WIFI_SECOND_CHAN_NONE);

    /* Restart ping */
    wifi_ping_router_start();

    s_chanscan_last = esp_timer_get_time();
    s_chanscan_active = false;

    /* Emit completion event */
    char event[128];
    int len = snprintf(event, sizeof(event),
        "EVENT,%s,chanscan=done channels=%d",
        s_hostname, CHANSCAN_CHANNELS);
    if (s_udp_socket >= 0) {
        sendto(s_udp_socket, event, len, 0,
            (struct sockaddr *)&s_dest_addr, sizeof(s_dest_addr));
    }
    ESP_LOGI(TAG, "CHANSCAN: complete, returned to channel %d", home_channel);
}

/* --- OTA --- */

static void ota_task(void *arg)
{
    char *url = (char *)arg;
    ESP_LOGI(TAG, "OTA: downloading from %s", url);

    s_led_mode = LED_OTA;

    esp_http_client_config_t http_cfg = {
        .url = url,
        .timeout_ms = 30000,
    };

    esp_https_ota_config_t ota_cfg = {
        .http_config = &http_cfg,
    };

    esp_err_t err = esp_https_ota(&ota_cfg);
    free(url);

    if (err == ESP_OK) {
        ESP_LOGI(TAG, "OTA: success, rebooting...");
        s_led_mode = LED_SOLID;
        vTaskDelay(pdMS_TO_TICKS(500));
        esp_restart();
    } else {
        ESP_LOGE(TAG, "OTA: failed: %s", esp_err_to_name(err));
        s_led_mode = LED_SLOW_BLINK;
        s_ota_in_progress = false;
    }

    vTaskDelete(NULL);
}

/* --- Promiscuous mode: deauth/disassoc detection + probe request capture --- */

typedef struct {
    uint16_t frame_ctrl;
    uint16_t duration;
    uint8_t  addr1[6];  /* destination */
    uint8_t  addr2[6];  /* source */
    uint8_t  addr3[6];  /* BSSID */
    uint16_t seq_ctrl;
} __attribute__((packed)) wifi_ieee80211_mac_hdr_t;

/* Probe request deduplication: report each MAC at most once per N seconds */
#define PROBE_DEDUP_SIZE  32
/* s_probe_dedup_us declared in globals section (before config_load_nvs) */

static struct {
    uint8_t mac[6];
    int64_t ts;
} s_probe_seen[PROBE_DEDUP_SIZE];

static bool probe_dedup_check(const uint8_t *mac)
{
    int64_t now = esp_timer_get_time();
    int oldest_idx = 0;
    int64_t oldest_ts = INT64_MAX;

    for (int i = 0; i < PROBE_DEDUP_SIZE; i++) {
        if (memcmp(s_probe_seen[i].mac, mac, 6) == 0) {
            if (now - s_probe_seen[i].ts < s_probe_dedup_us) {
                return true;  /* seen recently, skip */
            }
            s_probe_seen[i].ts = now;
            return false;  /* cooldown expired */
        }
        if (s_probe_seen[i].ts < oldest_ts) {
            oldest_ts = s_probe_seen[i].ts;
            oldest_idx = i;
        }
    }

    /* New MAC — replace oldest entry */
    memcpy(s_probe_seen[oldest_idx].mac, mac, 6);
    s_probe_seen[oldest_idx].ts = now;
    return false;
}

static int deauth_flood_check(void)
{
    int64_t now = esp_timer_get_time();
    int64_t window_us = (int64_t)s_flood_window_s * 1000000LL;

    /* Record this event */
    s_deauth_ring[s_deauth_ring_head].ts = now;
    s_deauth_ring_head = (s_deauth_ring_head + 1) % FLOOD_RING_SIZE;
    if (s_deauth_ring_count < FLOOD_RING_SIZE) {
        s_deauth_ring_count++;
    }

    /* Count events within window */
    int count = 0;
    for (int i = 0; i < s_deauth_ring_count; i++) {
        if (now - s_deauth_ring[i].ts <= window_us) {
            count++;
        }
    }
    return count;
}

static void wifi_promiscuous_cb(void *buf, wifi_promiscuous_pkt_type_t type)
{
    if (type != WIFI_PKT_MGMT) return;

    const wifi_promiscuous_pkt_t *pkt = (wifi_promiscuous_pkt_t *)buf;
    const wifi_ieee80211_mac_hdr_t *hdr = (wifi_ieee80211_mac_hdr_t *)pkt->payload;

    uint8_t subtype = (hdr->frame_ctrl >> 4) & 0x0F;

    /* Deauth (0x0C) / Disassoc (0x0A) */
    if (subtype == 0x0C || subtype == 0x0A) {
        const char *type_str = (subtype == 0x0C) ? "deauth" : "disassoc";

        int flood_count = deauth_flood_check();
        char alert[192];
        int len;

        if (flood_count >= s_flood_thresh) {
            /* Flood detected — send aggregate alert, suppress individual */
            int64_t now = esp_timer_get_time();
            if (!s_flood_active || (now - s_flood_alert_ts > 5000000LL)) {
                /* Send flood alert at most every 5 seconds */
                s_flood_active = true;
                s_flood_alert_ts = now;
                len = snprintf(alert, sizeof(alert),
                    "ALERT_DATA,%s,deauth_flood,%d,%d\n",
                    s_hostname, flood_count, s_flood_window_s);
                if (s_udp_socket >= 0) {
                    sendto(s_udp_socket, alert, len, 0,
                        (struct sockaddr *)&s_dest_addr, sizeof(s_dest_addr));
                }
                ESP_LOGW(TAG, "ALERT: deauth_flood count=%d window=%ds", flood_count, s_flood_window_s);
            }
        } else {
            /* Below threshold — send individual alert */
            if (s_flood_active) {
                s_flood_active = false;
            }
            len = snprintf(alert, sizeof(alert),
                "ALERT_DATA,%s,%s,"
                "%02x:%02x:%02x:%02x:%02x:%02x,"
                "%02x:%02x:%02x:%02x:%02x:%02x,"
                "%d\n",
                s_hostname, type_str,
                hdr->addr2[0], hdr->addr2[1], hdr->addr2[2],
                hdr->addr2[3], hdr->addr2[4], hdr->addr2[5],
                hdr->addr1[0], hdr->addr1[1], hdr->addr1[2],
                hdr->addr1[3], hdr->addr1[4], hdr->addr1[5],
                pkt->rx_ctrl.rssi);

            if (s_udp_socket >= 0) {
                sendto(s_udp_socket, alert, len, 0,
                    (struct sockaddr *)&s_dest_addr, sizeof(s_dest_addr));
            }

            ESP_LOGW(TAG, "ALERT: %s from " MACSTR " -> " MACSTR " rssi=%d",
                     type_str,
                     hdr->addr2[0], hdr->addr2[1], hdr->addr2[2],
                     hdr->addr2[3], hdr->addr2[4], hdr->addr2[5],
                     hdr->addr1[0], hdr->addr1[1], hdr->addr1[2],
                     hdr->addr1[3], hdr->addr1[4], hdr->addr1[5],
                     pkt->rx_ctrl.rssi);
        }
        return;
    }

    /* Probe request (0x04) */
    if (subtype == 0x04) {
        /* Dedup: skip if this MAC was reported recently */
        if (probe_dedup_check(hdr->addr2)) return;

        /* Parse SSID from tagged parameters after MAC header */
        const uint8_t *body = pkt->payload + sizeof(wifi_ieee80211_mac_hdr_t);
        int body_len = pkt->rx_ctrl.sig_len - sizeof(wifi_ieee80211_mac_hdr_t);

        char ssid[33] = "";
        if (body_len >= 2 && body[0] == 0) {  /* Tag 0 = SSID */
            int ssid_len = body[1];
            if (ssid_len > 0 && ssid_len <= 32 && ssid_len + 2 <= body_len) {
                memcpy(ssid, &body[2], ssid_len);
                ssid[ssid_len] = '\0';
                /* Sanitize: replace non-printable and CSV-breaking chars */
                for (int j = 0; j < ssid_len; j++) {
                    if (ssid[j] < 0x20 || ssid[j] > 0x7e || ssid[j] == ',') {
                        ssid[j] = '?';
                    }
                }
            }
        }

        char probe[192];
        int len = snprintf(probe, sizeof(probe),
            "PROBE_DATA,%s,%02x:%02x:%02x:%02x:%02x:%02x,%d,%s,%d\n",
            s_hostname,
            hdr->addr2[0], hdr->addr2[1], hdr->addr2[2],
            hdr->addr2[3], hdr->addr2[4], hdr->addr2[5],
            pkt->rx_ctrl.rssi,
            ssid,
            pkt->rx_ctrl.channel);

        if (s_udp_socket >= 0) {
            sendto(s_udp_socket, probe, len, 0,
                (struct sockaddr *)&s_dest_addr, sizeof(s_dest_addr));
        }
    }
}

static void wifi_promiscuous_init(void)
{
    wifi_promiscuous_filter_t filt = {
        .filter_mask = WIFI_PROMIS_FILTER_MASK_MGMT,
    };
    ESP_ERROR_CHECK(esp_wifi_set_promiscuous_filter(&filt));
    ESP_ERROR_CHECK(esp_wifi_set_promiscuous_rx_cb(wifi_promiscuous_cb));
    ESP_ERROR_CHECK(esp_wifi_set_promiscuous(true));
    ESP_LOGI(TAG, "Promiscuous mode: deauth/disassoc/probe detection enabled");
}

/* --- HMAC command authentication --- */

/**
 * Verify HMAC-signed command. Format: "HMAC:<16hex>:<uptime_s>:<cmd>"
 * HMAC = truncated SHA-256(secret, "<uptime_s>:<cmd>")
 * Timestamp must be within 30s of device uptime (replay protection).
 * Returns pointer to actual command on success, or NULL on failure
 * (with error message written to reply).
 */
static const char *auth_verify(const char *input, char *reply, size_t reply_size)
{
    /* No secret configured — accept everything */
    if (s_auth_secret[0] == '\0') {
        return input;
    }

    /* Check for HMAC: prefix */
    if (strncmp(input, "HMAC:", 5) != 0) {
        snprintf(reply, reply_size, "ERR AUTH required");
        return NULL;
    }

    /* Parse: HMAC:<16 hex chars>:<uptime_s>:<cmd> */
    if (strlen(input) < 5 + 16 + 1) {
        snprintf(reply, reply_size, "ERR AUTH malformed");
        return NULL;
    }
    if (input[5 + 16] != ':') {
        snprintf(reply, reply_size, "ERR AUTH malformed");
        return NULL;
    }

    /* payload = "<uptime_s>:<cmd>" */
    const char *payload = input + 5 + 16 + 1;
    const char *cmd_sep = strchr(payload, ':');
    if (!cmd_sep) {
        snprintf(reply, reply_size, "ERR AUTH malformed (need timestamp:cmd)");
        return NULL;
    }

    /* Replay protection: reject stale timestamps */
    long ts = strtol(payload, NULL, 10);
    int64_t now_s = esp_timer_get_time() / 1000000LL;
    int64_t drift = now_s - (int64_t)ts;
    if (drift < -30 || drift > 30) {
        snprintf(reply, reply_size, "ERR AUTH expired (drift=%llds)", (long long)drift);
        return NULL;
    }

    const char *cmd = cmd_sep + 1;

    /* Compute HMAC-SHA256 over payload (timestamp:cmd) */
    uint8_t hmac[32];
    mbedtls_md_context_t ctx;
    mbedtls_md_init(&ctx);
    const mbedtls_md_info_t *md_info = mbedtls_md_info_from_type(MBEDTLS_MD_SHA256);
    mbedtls_md_setup(&ctx, md_info, 1);
    mbedtls_md_hmac_starts(&ctx, (const uint8_t *)s_auth_secret, strlen(s_auth_secret));
    mbedtls_md_hmac_update(&ctx, (const uint8_t *)payload, strlen(payload));
    mbedtls_md_hmac_finish(&ctx, hmac);
    mbedtls_md_free(&ctx);

    /* Format first 8 bytes as 16 hex chars */
    char expected[17];
    for (int i = 0; i < 8; i++) {
        snprintf(expected + i * 2, 3, "%02x", hmac[i]);
    }

    /* Constant-time comparison (prevents timing side-channel) */
    volatile uint8_t diff = 0;
    for (int i = 0; i < 16; i++) {
        diff |= (uint8_t)input[5 + i] ^ (uint8_t)expected[i];
    }
    if (diff != 0) {
        snprintf(reply, reply_size, "ERR AUTH failed");
        return NULL;
    }

    return cmd;
}

/* --- Privileged command check --- */

static bool cmd_requires_auth(const char *cmd)
{
    /* Whitelist: read-only / query commands that don't modify state */
    if (strcmp(cmd, "STATUS") == 0) return false;
    if (strcmp(cmd, "CONFIG") == 0) return false;
    if (strcmp(cmd, "PROFILE") == 0) return false;
    if (strcmp(cmd, "PING") == 0) return false;
    if (strcmp(cmd, "HELP") == 0) return false;
    if (strcmp(cmd, "HOSTNAME") == 0) return false;
    if (strcmp(cmd, "CSI") == 0) return false;
    if (strcmp(cmd, "CSIMODE") == 0) return false;
    if (strcmp(cmd, "POWERSAVE") == 0) return false;
    if (strcmp(cmd, "PRESENCE") == 0) return false;
    if (strcmp(cmd, "CHANSCAN") == 0) return false;
    if (strcmp(cmd, "FLOODTHRESH") == 0) return false;
    if (strcmp(cmd, "AUTH") == 0) return false;
    if (strcmp(cmd, "ALERT") == 0) return false;
    if (strcmp(cmd, "LED") == 0) return false;
    if (strcmp(cmd, "LOG") == 0) return false;
    if (strcmp(cmd, "CALIBRATE STATUS") == 0) return false;
    /* Everything else modifies state and requires auth */
    return true;
}

/* --- Command handler --- */

static void reboot_after_delay(void *arg)
{
    vTaskDelay(pdMS_TO_TICKS(200));
    esp_restart();
}

/* --- Power test --- */

static void send_powertest_event(const char *phase, int dwell_s)
{
    char buf[128];
    int len = snprintf(buf, sizeof(buf), "EVENT,%s,powertest,%s,%d\n",
                       s_hostname, phase, dwell_s);
    if (s_udp_socket >= 0) {
        sendto(s_udp_socket, buf, len, 0,
               (struct sockaddr *)&s_dest_addr, sizeof(s_dest_addr));
    }
}

static void powertest_task(void *arg)
{
    int dwell_s = (int)(intptr_t)arg;

    /* Save current settings */
    int saved_freq = s_send_frequency;
    bool saved_adaptive = s_adaptive;
    bool saved_ble = s_ble_enabled;
    int8_t saved_tx_power = s_tx_power_dbm;
    led_mode_t saved_led = s_led_mode;
    bool saved_powersave = s_powersave;

    /* Disable adaptive during test */
    s_adaptive = false;
    s_motion_detected = false;

    /* Disable powersave during test */
    if (s_powersave) {
        s_powersave = false;
        esp_wifi_set_ps(WIFI_PS_NONE);
    }

    typedef struct {
        const char *name;
        int rate;           /* 0 = stop ping */
        bool ble;
        int8_t tx_dbm;     /* 0 = no change */
        bool led;
    } phase_t;

    static const phase_t phases[] = {
        { "idle",     0,   false, 0,  false },
        { "csi_10",   10,  false, 0,  true  },
        { "csi_100",  100, false, 0,  true  },
        { "ble_only", 0,   true,  0,  true  },
        { "all",      100, true,  0,  true  },
        { "tx_low",   100, false, 2,  true  },
        { "tx_high",  100, false, 20, true  },
    };
    int n_phases = sizeof(phases) / sizeof(phases[0]);

    ESP_LOGI(TAG, "POWERTEST: starting %d phases, dwell=%ds", n_phases, dwell_s);

    for (int i = 0; i < n_phases; i++) {
        const phase_t *p = &phases[i];

        send_powertest_event(p->name, dwell_s);
        ESP_LOGI(TAG, "POWERTEST: phase %s (rate=%d ble=%d tx=%d)", p->name, p->rate, p->ble, p->tx_dbm);

        /* LED */
        s_led_mode = p->led ? LED_FAST_BLINK : LED_OFF;

        /* BLE */
        if (p->ble && !s_ble_enabled) {
            s_ble_enabled = true;
            ble_scan_start();
        } else if (!p->ble && s_ble_enabled) {
            s_ble_enabled = false;
            ble_gap_disc_cancel();
        }

        /* TX power */
        if (p->tx_dbm > 0) {
            s_tx_power_dbm = p->tx_dbm;
            esp_wifi_set_max_tx_power(s_tx_power_dbm * 4);
        }

        /* Ping rate */
        if (p->rate > 0) {
            s_send_frequency = p->rate;
            wifi_ping_router_start();
        } else {
            if (s_ping_handle) {
                esp_ping_stop(s_ping_handle);
                esp_ping_delete_session(s_ping_handle);
                s_ping_handle = NULL;
            }
        }

        vTaskDelay(pdMS_TO_TICKS(dwell_s * 1000));
    }

    /* Restore settings */
    s_adaptive = saved_adaptive;
    s_ble_enabled = saved_ble;
    s_tx_power_dbm = saved_tx_power;
    esp_wifi_set_max_tx_power(s_tx_power_dbm * 4);
    s_led_mode = saved_led;
    s_send_frequency = saved_freq;

    if (saved_ble) {
        ble_scan_start();
    } else {
        ble_gap_disc_cancel();
    }
    wifi_ping_router_start();

    s_powersave = saved_powersave;
    esp_wifi_set_ps(s_powersave ? WIFI_PS_MIN_MODEM : WIFI_PS_NONE);

    int total_s = dwell_s * n_phases;
    send_powertest_event("done", total_s);
    ESP_LOGI(TAG, "POWERTEST: done total=%ds", total_s);

    s_powertest_running = false;
    vTaskDelete(NULL);
}

static int cmd_handle(const char *cmd, char *reply, size_t reply_size)
{
    /* REBOOT */
    if (strncmp(cmd, "REBOOT", 6) == 0) {
        snprintf(reply, reply_size, "OK REBOOTING");
        xTaskCreate(reboot_after_delay, "reboot", 1024, NULL, 1, NULL);
        return strlen(reply);
    }

    /* IDENTIFY */
    if (strncmp(cmd, "IDENTIFY", 8) == 0) {
        s_identify_end_time = esp_timer_get_time() + (5 * 1000000LL);
        s_led_mode = LED_SOLID;
        snprintf(reply, reply_size, "OK IDENTIFY 5s");
        return strlen(reply);
    }

    /* LED [QUIET|AUTO] */
    if (strncmp(cmd, "LED", 3) == 0) {
        if (cmd[3] == '\0' || cmd[3] == '\n') {
            snprintf(reply, reply_size, "OK LED %s", s_led_quiet ? "quiet" : "auto");
            return strlen(reply);
        }
        if (strncmp(cmd + 4, "QUIET", 5) == 0) {
            s_led_quiet = true;
            s_led_mode = LED_OFF;
            config_save_i8("led_quiet", 1);
            snprintf(reply, reply_size, "OK LED quiet (off, solid on motion)");
            return strlen(reply);
        }
        if (strncmp(cmd + 4, "AUTO", 4) == 0) {
            s_led_quiet = false;
            s_led_mode = LED_SLOW_BLINK;
            config_save_i8("led_quiet", 0);
            snprintf(reply, reply_size, "OK LED auto (blink)");
            return strlen(reply);
        }
        snprintf(reply, reply_size, "ERR LED [QUIET|AUTO]");
        return strlen(reply);
    }

    /* STATUS */
    if (strncmp(cmd, "STATUS", 6) == 0) {
        int64_t up = esp_timer_get_time() / 1000000LL;
        int days = (int)(up / 86400);
        int hours = (int)((up % 86400) / 3600);
        int mins = (int)((up % 3600) / 60);
        uint32_t heap = esp_get_free_heap_size();

        wifi_ap_record_t ap;
        int rssi = 0;
        int channel = 0;
        if (esp_wifi_sta_get_ap_info(&ap) == ESP_OK) {
            rssi = ap.rssi;
            channel = ap.primary;
        }

        const esp_app_desc_t *app_desc = esp_app_get_description();

        float chip_temp = 0.0f;
#if SOC_TEMP_SENSOR_SUPPORTED
        if (s_temp_handle) {
            temperature_sensor_get_celsius(s_temp_handle, &chip_temp);
        }
#endif

        int actual_rate = (up > 0) ? (int)((uint64_t)s_csi_count / (uint64_t)up) : 0;

        char uptime_str[32];
        if (days > 0) {
            snprintf(uptime_str, sizeof(uptime_str), "%dd%dh%dm", days, hours, mins);
        } else if (hours > 0) {
            snprintf(uptime_str, sizeof(uptime_str), "%dh%dm", hours, mins);
        } else {
            snprintf(uptime_str, sizeof(uptime_str), "%dm", mins);
        }

        const char *csi_mode_str = (s_csi_mode == CSI_MODE_COMPACT) ? "compact" :
                                   (s_csi_mode == CSI_MODE_HYBRID)  ? "hybrid" : "raw";

        /* NVS stats */
        nvs_stats_t nvs_stats = {0};
        nvs_get_stats("nvs", &nvs_stats);

        /* Partition info */
        const esp_partition_t *running = esp_ota_get_running_partition();
        uint32_t part_size = running ? running->size : 0;

        /* Chip info */
        esp_chip_info_t chip_info;
        esp_chip_info(&chip_info);
        const char *chip_model = (chip_info.model == CHIP_ESP32S3) ? "ESP32S3" :
                                 (chip_info.model == CHIP_ESP32C3) ? "ESP32C3" :
                                 (chip_info.model == CHIP_ESP32)   ? "ESP32" : "ESP32xx";

        snprintf(reply, reply_size,
                 "OK STATUS uptime=%s uptime_s=%lld heap=%lu rssi=%d channel=%d tx_power=%d rate=%d csi_rate=%d"
                 " hostname=%s version=%s adaptive=%s motion=%d ble=%s target=%s:%d"
                 " temp=%.1f csi_count=%lu boots=%lu rssi_min=%d rssi_max=%d"
                 " csi=%s csi_mode=%s hybrid_n=%d auth=%s flood_thresh=%d/%d powersave=%s"
                 " presence=%s pr_score=%.4f chanscan=%s led=%s"
                 " alert_temp=%.1f alert_heap=%lu"
                 " nvs_used=%lu nvs_free=%lu nvs_total=%lu part_size=%lu"
                 " built=%s_%s idf=%s chip=%sr%dc%d",
                 uptime_str, (long long)up, (unsigned long)heap, rssi, channel, (int)s_tx_power_dbm,
                 s_send_frequency, actual_rate,
                 s_hostname, app_desc->version,
                 s_adaptive ? "on" : "off", s_motion_detected ? 1 : 0,
                 s_ble_enabled ? "on" : "off", s_target_ip, s_target_port,
                 chip_temp, (unsigned long)s_csi_count, (unsigned long)s_boot_count,
                 (int)s_rssi_min, (int)s_rssi_max,
                 s_csi_enabled ? "on" : "off", csi_mode_str, s_hybrid_interval,
                 s_auth_secret[0] ? "on" : "off",
                 s_flood_thresh, s_flood_window_s,
                 s_powersave ? "on" : "off",
                 s_presence_enabled ? "on" : "off", s_pr_last_score,
                 s_chanscan_enabled ? "on" : "off", s_led_quiet ? "quiet" : "auto",
                 s_alert_temp_thresh, (unsigned long)s_alert_heap_thresh,
                 (unsigned long)nvs_stats.used_entries,
                 (unsigned long)nvs_stats.free_entries,
                 (unsigned long)nvs_stats.total_entries,
                 (unsigned long)part_size,
                 app_desc->date, app_desc->time, app_desc->idf_ver,
                 chip_model, chip_info.revision, chip_info.cores);
        return strlen(reply);
    }

    /* RATE <10-100> */
    if (strncmp(cmd, "RATE ", 5) == 0) {
        int val = atoi(cmd + 5);
        if (val < 10 || val > 100) {
            snprintf(reply, reply_size, "ERR RATE range 10-100");
            return strlen(reply);
        }
        if (s_adaptive) {
            s_adaptive = false;
            s_motion_detected = false;
            config_save_i8("adaptive", 0);
        }
        s_send_frequency = val;
        config_save_i32("send_rate", (int32_t)val);
        wifi_ping_router_start();
        snprintf(reply, reply_size, "OK RATE %d (adaptive off)", val);
        return strlen(reply);
    }

    /* POWER <2-20> */
    if (strncmp(cmd, "POWER ", 6) == 0) {
        int val = atoi(cmd + 6);
        if (val < 2 || val > 20) {
            snprintf(reply, reply_size, "ERR POWER range 2-20");
            return strlen(reply);
        }
        s_tx_power_dbm = (int8_t)val;
        esp_wifi_set_max_tx_power(s_tx_power_dbm * 4);
        config_save_i8("tx_power", s_tx_power_dbm);
        snprintf(reply, reply_size, "OK POWER %d dBm", val);
        return strlen(reply);
    }

    /* TARGET <ip> [port] */
    if (strncmp(cmd, "TARGET ", 7) == 0) {
        char ip_buf[16] = {0};
        int port = s_target_port;
        /* parse: "TARGET 192.168.1.10" or "TARGET 192.168.1.10 5500" */
        if (sscanf(cmd + 7, "%15s %d", ip_buf, &port) < 1) {
            snprintf(reply, reply_size, "ERR TARGET <ip> [port]");
            return strlen(reply);
        }
        /* validate IP */
        struct in_addr test_addr;
        if (inet_pton(AF_INET, ip_buf, &test_addr) != 1) {
            snprintf(reply, reply_size, "ERR TARGET invalid IP");
            return strlen(reply);
        }
        if (port < 1 || port > 65535) {
            snprintf(reply, reply_size, "ERR TARGET port range 1-65535");
            return strlen(reply);
        }
        strncpy(s_target_ip, ip_buf, sizeof(s_target_ip) - 1);
        s_target_ip[sizeof(s_target_ip) - 1] = '\0';
        s_target_port = (uint16_t)port;
        /* update live socket destination */
        s_dest_addr.sin_port = htons(s_target_port);
        inet_pton(AF_INET, s_target_ip, &s_dest_addr.sin_addr);
        /* persist */
        config_save_str("target_ip", s_target_ip);
        config_save_i32("target_port", (int32_t)s_target_port);
        snprintf(reply, reply_size, "OK TARGET %s:%d", s_target_ip, s_target_port);
        return strlen(reply);
    }

    /* HOSTNAME <name> */
    if (strncmp(cmd, "HOSTNAME ", 9) == 0) {
        const char *name = cmd + 9;
        size_t nlen = strlen(name);
        if (nlen == 0 || nlen >= sizeof(s_hostname)) {
            snprintf(reply, reply_size, "ERR HOSTNAME length 1-%d", (int)sizeof(s_hostname) - 1);
            return strlen(reply);
        }
        /* Validate: lowercase alphanumeric and hyphens only */
        for (size_t i = 0; i < nlen; i++) {
            char c = name[i];
            if (!((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-')) {
                snprintf(reply, reply_size, "ERR HOSTNAME chars: a-z 0-9 -");
                return strlen(reply);
            }
        }
        strncpy(s_hostname, name, sizeof(s_hostname) - 1);
        s_hostname[sizeof(s_hostname) - 1] = '\0';
        config_save_str("hostname", s_hostname);
        mdns_hostname_set(s_hostname);
        snprintf(reply, reply_size, "OK HOSTNAME %s (mDNS updated, reboot recommended)", s_hostname);
        return strlen(reply);
    }
    if (strcmp(cmd, "HOSTNAME") == 0) {
        snprintf(reply, reply_size, "OK HOSTNAME %s", s_hostname);
        return strlen(reply);
    }

    /* BLE ON/OFF */
    if (strncmp(cmd, "BLE ", 4) == 0) {
        const char *arg = cmd + 4;
        if (strncmp(arg, "ON", 2) == 0) {
            s_ble_enabled = true;
            config_save_i8("ble_scan", 1);
            ble_scan_start();
            snprintf(reply, reply_size, "OK BLE scanning on");
        } else if (strncmp(arg, "OFF", 3) == 0) {
            s_ble_enabled = false;
            config_save_i8("ble_scan", 0);
            ble_gap_disc_cancel();
            snprintf(reply, reply_size, "OK BLE scanning off");
        } else {
            snprintf(reply, reply_size, "ERR BLE ON or OFF");
        }
        return strlen(reply);
    }

    /* ADAPTIVE ON/OFF */
    if (strncmp(cmd, "ADAPTIVE ", 9) == 0) {
        const char *arg = cmd + 9;
        if (strncmp(arg, "ON", 2) == 0) {
            s_adaptive = true;
            s_energy_idx = 0;
            config_save_i8("adaptive", 1);
            snprintf(reply, reply_size, "OK ADAPTIVE on threshold=%.6f", s_motion_threshold);
        } else if (strncmp(arg, "OFF", 3) == 0) {
            s_adaptive = false;
            s_motion_detected = false;
            config_save_i8("adaptive", 0);
            snprintf(reply, reply_size, "OK ADAPTIVE off");
        } else {
            snprintf(reply, reply_size, "ERR ADAPTIVE ON or OFF");
        }
        return strlen(reply);
    }

    /* THRESHOLD <value> */
    if (strncmp(cmd, "THRESHOLD ", 10) == 0) {
        float val = strtof(cmd + 10, NULL);
        if (val <= 0.0f || val > 1.0f) {
            snprintf(reply, reply_size, "ERR THRESHOLD range 0.000001-1.0");
            return strlen(reply);
        }
        s_motion_threshold = val;
        config_save_i32("threshold", (int32_t)(val * 1000000.0f));
        snprintf(reply, reply_size, "OK THRESHOLD %.6f", val);
        return strlen(reply);
    }

    /* SCANRATE <5-300> */
    if (strncmp(cmd, "SCANRATE ", 9) == 0) {
        int val = atoi(cmd + 9);
        if (val < 5 || val > 300) {
            snprintf(reply, reply_size, "ERR SCANRATE range 5-300 seconds");
            return strlen(reply);
        }
        s_ble_scan_interval_us = (int64_t)val * 1000000LL;
        config_save_i32("scan_rate", (int32_t)val);
        if (s_ble_timer) {
            esp_timer_stop(s_ble_timer);
            esp_timer_start_periodic(s_ble_timer, s_ble_scan_interval_us);
        }
        snprintf(reply, reply_size, "OK SCANRATE %ds (saved)", val);
        return strlen(reply);
    }

    /* PROBERATE <1-300> */
    if (strncmp(cmd, "PROBERATE ", 10) == 0) {
        int val = atoi(cmd + 10);
        if (val < 1 || val > 300) {
            snprintf(reply, reply_size, "ERR PROBERATE range 1-300 seconds");
            return strlen(reply);
        }
        s_probe_dedup_us = (int64_t)val * 1000000LL;
        config_save_i32("probe_rate", (int32_t)val);
        snprintf(reply, reply_size, "OK PROBERATE %ds (saved)", val);
        return strlen(reply);
    }

    /* CSI [ON|OFF] - enable/disable CSI collection */
    if (strcmp(cmd, "CSI") == 0) {
        snprintf(reply, reply_size, "OK CSI %s", s_csi_enabled ? "on" : "off");
        return strlen(reply);
    }
    if (strncmp(cmd, "CSI ", 4) == 0) {
        const char *arg = cmd + 4;
        if (strncmp(arg, "ON", 2) == 0) {
            s_csi_enabled = true;
            config_save_i8("csi_enabled", 1);
            wifi_ping_router_start();
            snprintf(reply, reply_size, "OK CSI on");
        } else if (strncmp(arg, "OFF", 3) == 0) {
            s_csi_enabled = false;
            config_save_i8("csi_enabled", 0);
            snprintf(reply, reply_size, "OK CSI off (probe capture active)");
        } else {
            snprintf(reply, reply_size, "ERR CSI ON|OFF");
        }
        return strlen(reply);
    }

    /* CSIMODE [RAW|COMPACT|HYBRID N] */
    if (strcmp(cmd, "CSIMODE") == 0) {
        const char *mode_str = (s_csi_mode == CSI_MODE_COMPACT) ? "COMPACT" :
                               (s_csi_mode == CSI_MODE_HYBRID)  ? "HYBRID" : "RAW";
        if (s_csi_mode == CSI_MODE_HYBRID) {
            snprintf(reply, reply_size, "OK CSIMODE %s %d", mode_str, s_hybrid_interval);
        } else {
            snprintf(reply, reply_size, "OK CSIMODE %s", mode_str);
        }
        return strlen(reply);
    }
    if (strncmp(cmd, "CSIMODE ", 8) == 0) {
        const char *arg = cmd + 8;
        if (strncmp(arg, "RAW", 3) == 0) {
            s_csi_mode = CSI_MODE_RAW;
            config_save_i8("csi_mode", (int8_t)CSI_MODE_RAW);
            snprintf(reply, reply_size, "OK CSIMODE RAW");
        } else if (strncmp(arg, "COMPACT", 7) == 0) {
            s_csi_mode = CSI_MODE_COMPACT;
            config_save_i8("csi_mode", (int8_t)CSI_MODE_COMPACT);
            snprintf(reply, reply_size, "OK CSIMODE COMPACT");
        } else if (strncmp(arg, "HYBRID", 6) == 0) {
            int n = 10;
            if (arg[6] == ' ') {
                n = atoi(arg + 7);
            }
            if (n < 1 || n > 100) {
                snprintf(reply, reply_size, "ERR CSIMODE HYBRID N range 1-100");
                return strlen(reply);
            }
            s_csi_mode = CSI_MODE_HYBRID;
            s_hybrid_interval = n;
            config_save_i8("csi_mode", (int8_t)CSI_MODE_HYBRID);
            config_save_i32("hybrid_n", (int32_t)n);
            snprintf(reply, reply_size, "OK CSIMODE HYBRID %d", n);
        } else {
            snprintf(reply, reply_size, "ERR CSIMODE RAW|COMPACT|HYBRID [N]");
        }
        return strlen(reply);
    }

    /* PROFILE */
    if (strcmp(cmd, "PROFILE") == 0) {
        int pos = 0;
        /* Heap info */
        size_t free_heap = esp_get_free_heap_size();
        size_t min_heap = esp_get_minimum_free_heap_size();
        size_t free_dram = heap_caps_get_free_size(MALLOC_CAP_8BIT);
        size_t total_dram = heap_caps_get_total_size(MALLOC_CAP_8BIT);
        size_t free_iram = heap_caps_get_free_size(MALLOC_CAP_IRAM_8BIT);
        pos += snprintf(reply + pos, reply_size - pos,
                        "OK PROFILE\nHEAP free=%u min=%u dram=%u/%u iram=%u\n",
                        (unsigned)free_heap, (unsigned)min_heap,
                        (unsigned)free_dram, (unsigned)total_dram, (unsigned)free_iram);

        /* Per-task stack watermarks */
        pos += snprintf(reply + pos, reply_size - pos, "TASKS\n");
        const char *task_names[] = {"led_task", "cmd_task", "adaptive", "ble_host", "main", NULL};
        for (int i = 0; task_names[i] != NULL && pos < (int)reply_size - 60; i++) {
            TaskHandle_t th = xTaskGetHandle(task_names[i]);
            if (th) {
                UBaseType_t hwm = uxTaskGetStackHighWaterMark(th);
                pos += snprintf(reply + pos, reply_size - pos,
                                "  %-12s stack_free=%u\n", task_names[i], (unsigned)(hwm * sizeof(StackType_t)));
            }
        }

#if defined(CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS) && CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS
        /* CPU runtime stats */
        UBaseType_t n = uxTaskGetNumberOfTasks();
        TaskStatus_t *tasks = malloc(n * sizeof(TaskStatus_t));
        if (tasks) {
            uint32_t total_time;
            n = uxTaskGetSystemState(tasks, n, &total_time);
            if (total_time > 0) {
                pos += snprintf(reply + pos, reply_size - pos, "CPU\n");
                for (UBaseType_t i = 0; i < n && pos < (int)reply_size - 60; i++) {
                    uint32_t pct = (tasks[i].ulRunTimeCounter * 100) / total_time;
                    if (pct > 0 || tasks[i].ulRunTimeCounter > 0) {
                        pos += snprintf(reply + pos, reply_size - pos,
                                        "  %-12s %3lu%%\n", tasks[i].pcTaskName, (unsigned long)pct);
                    }
                }
            }
            free(tasks);
        }
#endif
        return pos;
    }

    /* FLOODTHRESH [count [window_s]] */
    if (strcmp(cmd, "FLOODTHRESH") == 0) {
        snprintf(reply, reply_size, "OK FLOODTHRESH %d/%ds", s_flood_thresh, s_flood_window_s);
        return strlen(reply);
    }
    if (strncmp(cmd, "FLOODTHRESH ", 12) == 0) {
        int count = 0, window = s_flood_window_s;
        if (sscanf(cmd + 12, "%d %d", &count, &window) < 1 || count < 1 || count > 100) {
            snprintf(reply, reply_size, "ERR FLOODTHRESH <1-100> [window_s 1-300]");
            return strlen(reply);
        }
        if (window < 1 || window > 300) {
            snprintf(reply, reply_size, "ERR FLOODTHRESH window range 1-300");
            return strlen(reply);
        }
        s_flood_thresh = count;
        s_flood_window_s = window;
        config_save_i32("flood_thresh", (int32_t)count);
        config_save_i32("flood_window", (int32_t)window);
        snprintf(reply, reply_size, "OK FLOODTHRESH %d/%ds", s_flood_thresh, s_flood_window_s);
        return strlen(reply);
    }

    /* AUTH [secret] — rotate secret (disable not allowed remotely) */
    if (strcmp(cmd, "AUTH") == 0) {
        snprintf(reply, reply_size, "OK AUTH %s", s_auth_secret[0] ? "on" : "off");
        return strlen(reply);
    }
    if (strncmp(cmd, "AUTH ", 5) == 0) {
        const char *arg = cmd + 5;
        if (strcmp(arg, "OFF") == 0) {
            snprintf(reply, reply_size, "ERR AUTH cannot be disabled remotely (use FACTORY to reset)");
            return strlen(reply);
        } else {
            size_t alen = strlen(arg);
            if (alen < 8 || alen > 64) {
                snprintf(reply, reply_size, "ERR AUTH secret length 8-64");
                return strlen(reply);
            }
            strncpy(s_auth_secret, arg, sizeof(s_auth_secret) - 1);
            s_auth_secret[sizeof(s_auth_secret) - 1] = '\0';
            config_save_str("auth_secret", s_auth_secret);
            snprintf(reply, reply_size, "OK AUTH on");
        }
        return strlen(reply);
    }

    /* OTA <url> */
    if (strncmp(cmd, "OTA ", 4) == 0) {
        const char *url = cmd + 4;
        if (strncmp(url, "http://", 7) != 0 && strncmp(url, "https://", 8) != 0) {
            snprintf(reply, reply_size, "ERR OTA url must start with http:// or https://");
            return strlen(reply);
        }
        if (s_ota_in_progress) {
            snprintf(reply, reply_size, "ERR OTA already in progress");
            return strlen(reply);
        }
        char *url_copy = strdup(url);
        if (!url_copy) {
            snprintf(reply, reply_size, "ERR OTA out of memory");
            return strlen(reply);
        }
        s_ota_in_progress = true;
        xTaskCreate(ota_task, "ota_task", 8192, url_copy, 5, NULL);
        snprintf(reply, reply_size, "OK OTA started");
        return strlen(reply);
    }

    /* POWERTEST [dwell_s] */
    if (strncmp(cmd, "POWERTEST", 9) == 0) {
        if (s_powertest_running) {
            snprintf(reply, reply_size, "ERR POWERTEST already running");
            return strlen(reply);
        }
        int dwell = 15;
        if (cmd[9] == ' ') {
            dwell = atoi(cmd + 10);
            if (dwell < 5 || dwell > 60) {
                snprintf(reply, reply_size, "ERR POWERTEST dwell range 5-60");
                return strlen(reply);
            }
        }
        s_powertest_running = true;
        xTaskCreate(powertest_task, "powertest", 4096, (void *)(intptr_t)dwell, 3, NULL);
        snprintf(reply, reply_size, "OK POWERTEST started dwell=%ds phases=7 total=~%ds", dwell, dwell * 7);
        return strlen(reply);
    }

    /* POWERSAVE [ON|OFF] */
    if (strcmp(cmd, "POWERSAVE") == 0) {
        snprintf(reply, reply_size, "OK POWERSAVE %s", s_powersave ? "on" : "off");
        return strlen(reply);
    }
    if (strncmp(cmd, "POWERSAVE ", 10) == 0) {
        const char *arg = cmd + 10;
        if (strncmp(arg, "ON", 2) == 0) {
            s_powersave = true;
            esp_wifi_set_ps(WIFI_PS_MIN_MODEM);
            config_save_i8("powersave", 1);
            snprintf(reply, reply_size, "OK POWERSAVE on (modem sleep)");
        } else if (strncmp(arg, "OFF", 3) == 0) {
            s_powersave = false;
            esp_wifi_set_ps(WIFI_PS_NONE);
            config_save_i8("powersave", 0);
            snprintf(reply, reply_size, "OK POWERSAVE off");
        } else {
            snprintf(reply, reply_size, "ERR POWERSAVE ON or OFF");
        }
        return strlen(reply);
    }

    /* CALIBRATE [seconds | STATUS | CLEAR] */
    if (strcmp(cmd, "CALIBRATE") == 0 || strncmp(cmd, "CALIBRATE ", 10) == 0) {
        const char *arg = (cmd[9] == ' ') ? cmd + 10 : "";

        if (strcmp(arg, "STATUS") == 0) {
            if (s_calibrating) {
                snprintf(reply, reply_size, "OK CALIBRATE in_progress packets=%lu target=%d",
                         (unsigned long)s_calib_count, s_calib_target);
            } else if (s_baseline_nsub > 0) {
                snprintf(reply, reply_size, "OK CALIBRATE baseline valid nsub=%d", s_baseline_nsub);
            } else {
                snprintf(reply, reply_size, "OK CALIBRATE baseline none");
            }
            return strlen(reply);
        }

        if (strcmp(arg, "CLEAR") == 0) {
            s_baseline_nsub = 0;
            s_presence_enabled = false;
            s_presence_detected = false;
            s_pr_last_score = 0.0f;
            config_erase_key("bl_amps");
            config_erase_key("bl_nsub");
            config_save_i8("presence", 0);
            snprintf(reply, reply_size, "OK CALIBRATE cleared (presence off)");
            return strlen(reply);
        }

        if (s_calibrating) {
            snprintf(reply, reply_size, "ERR CALIBRATE already running");
            return strlen(reply);
        }
        if (s_csi_count < 200) {
            snprintf(reply, reply_size, "ERR CALIBRATE need 200+ CSI packets first (have %lu)",
                     (unsigned long)s_csi_count);
            return strlen(reply);
        }

        int seconds = 10;
        if (arg[0] != '\0') {
            seconds = atoi(arg);
        }
        if (seconds < 3 || seconds > 60) {
            snprintf(reply, reply_size, "ERR CALIBRATE range 3-60 seconds");
            return strlen(reply);
        }

        /* Start calibration */
        s_calib_count = 0;
        s_calib_nsub = 0;
        s_calib_target = seconds * s_send_frequency;
        if (s_calib_target < 30) s_calib_target = 30;
        s_calibrating = true;
        snprintf(reply, reply_size, "OK CALIBRATE started %ds target=%d packets", seconds, s_calib_target);
        return strlen(reply);
    }

    /* PRESENCE [ON|OFF|THRESHOLD <val>] */
    if (strcmp(cmd, "PRESENCE") == 0) {
        snprintf(reply, reply_size, "OK PRESENCE %s baseline=%s threshold=%.4f score=%.4f",
                 s_presence_enabled ? "on" : "off",
                 s_baseline_nsub > 0 ? "yes" : "no",
                 s_pr_threshold, s_pr_last_score);
        return strlen(reply);
    }
    if (strncmp(cmd, "PRESENCE ", 9) == 0) {
        const char *arg = cmd + 9;
        if (strncmp(arg, "ON", 2) == 0) {
            if (s_baseline_nsub == 0) {
                snprintf(reply, reply_size, "ERR PRESENCE need baseline (run CALIBRATE first)");
                return strlen(reply);
            }
            s_presence_enabled = true;
            s_presence_detected = false;
            s_pr_last_score = 0.0f;
            s_pr_score_idx = 0;
            s_last_presence_time = 0;
            config_save_i8("presence", 1);
            snprintf(reply, reply_size, "OK PRESENCE on threshold=%.4f", s_pr_threshold);
        } else if (strncmp(arg, "OFF", 3) == 0) {
            s_presence_enabled = false;
            s_presence_detected = false;
            s_pr_last_score = 0.0f;
            config_save_i8("presence", 0);
            snprintf(reply, reply_size, "OK PRESENCE off");
        } else if (strncmp(arg, "THRESHOLD ", 10) == 0) {
            float val = strtof(arg + 10, NULL);
            if (val < 0.001f || val > 1.0f) {
                snprintf(reply, reply_size, "ERR PRESENCE THRESHOLD range 0.001-1.0");
                return strlen(reply);
            }
            s_pr_threshold = val;
            config_save_i32("pr_thresh", (int32_t)(val * 1000000.0f));
            snprintf(reply, reply_size, "OK PRESENCE THRESHOLD %.4f", val);
        } else {
            snprintf(reply, reply_size, "ERR PRESENCE ON|OFF|THRESHOLD <0.001-1.0>");
        }
        return strlen(reply);
    }

    /* CHANSCAN [ON|OFF|NOW|INTERVAL <60-3600>] */
    if (strcmp(cmd, "CHANSCAN") == 0) {
        snprintf(reply, reply_size, "OK CHANSCAN %s interval=%ds active=%s",
                 s_chanscan_enabled ? "on" : "off",
                 s_chanscan_interval_s,
                 s_chanscan_active ? "yes" : "no");
        return strlen(reply);
    }
    if (strncmp(cmd, "CHANSCAN ", 9) == 0) {
        const char *arg = cmd + 9;
        if (strncmp(arg, "ON", 2) == 0) {
            s_chanscan_enabled = true;
            config_save_i8("chanscan", 1);
            snprintf(reply, reply_size, "OK CHANSCAN on interval=%ds", s_chanscan_interval_s);
        } else if (strncmp(arg, "OFF", 3) == 0) {
            s_chanscan_enabled = false;
            config_save_i8("chanscan", 0);
            snprintf(reply, reply_size, "OK CHANSCAN off");
        } else if (strncmp(arg, "NOW", 3) == 0) {
            if (s_chanscan_active) {
                snprintf(reply, reply_size, "ERR CHANSCAN already in progress");
            } else {
                channel_scan_run();
                snprintf(reply, reply_size, "OK CHANSCAN triggered");
            }
        } else if (strncmp(arg, "INTERVAL ", 9) == 0) {
            int val = atoi(arg + 9);
            if (val < 60 || val > 3600) {
                snprintf(reply, reply_size, "ERR CHANSCAN INTERVAL range 60-3600 seconds");
                return strlen(reply);
            }
            s_chanscan_interval_s = val;
            config_save_i32("chanscan_int", (int32_t)val);
            snprintf(reply, reply_size, "OK CHANSCAN INTERVAL %ds (saved)", val);
        } else {
            snprintf(reply, reply_size, "ERR CHANSCAN ON|OFF|NOW|INTERVAL <60-3600>");
        }
        return strlen(reply);
    }

    /* PING — echo reply for connectivity tests */
    if (strcmp(cmd, "PING") == 0) {
        snprintf(reply, reply_size, "OK PONG");
        return strlen(reply);
    }

    /* LOG <NONE|ERROR|WARN|INFO|DEBUG|VERBOSE> */
    if (strncmp(cmd, "LOG ", 4) == 0) {
        const char *arg = cmd + 4;
        esp_log_level_t level;
        if (strcmp(arg, "NONE") == 0)         level = ESP_LOG_NONE;
        else if (strcmp(arg, "ERROR") == 0)    level = ESP_LOG_ERROR;
        else if (strcmp(arg, "WARN") == 0)     level = ESP_LOG_WARN;
        else if (strcmp(arg, "INFO") == 0)     level = ESP_LOG_INFO;
        else if (strcmp(arg, "DEBUG") == 0)    level = ESP_LOG_DEBUG;
        else if (strcmp(arg, "VERBOSE") == 0)  level = ESP_LOG_VERBOSE;
        else {
            snprintf(reply, reply_size, "ERR LOG NONE|ERROR|WARN|INFO|DEBUG|VERBOSE");
            return strlen(reply);
        }
        esp_log_level_set("*", level);
        snprintf(reply, reply_size, "OK LOG %s", arg);
        return strlen(reply);
    }
    if (strcmp(cmd, "LOG") == 0) {
        snprintf(reply, reply_size, "OK LOG (use LOG <NONE|ERROR|WARN|INFO|DEBUG|VERBOSE>)");
        return strlen(reply);
    }

    /* RSSI RESET — reset min/max counters */
    if (strcmp(cmd, "RSSI RESET") == 0) {
        s_rssi_min = 0;
        s_rssi_max = -128;
        snprintf(reply, reply_size, "OK RSSI min/max reset");
        return strlen(reply);
    }

    /* ALERT — set temp/heap alert thresholds */
    if (strcmp(cmd, "ALERT") == 0) {
        snprintf(reply, reply_size, "OK ALERT temp=%.1f heap=%lu (0=off)",
                 s_alert_temp_thresh, (unsigned long)s_alert_heap_thresh);
        return strlen(reply);
    }
    if (strncmp(cmd, "ALERT ", 6) == 0) {
        const char *arg = cmd + 6;
        if (strncmp(arg, "TEMP ", 5) == 0) {
            float val = strtof(arg + 5, NULL);
            if (val < 0 || val > 125) {
                snprintf(reply, reply_size, "ERR ALERT TEMP range 0-125 (0=off)");
                return strlen(reply);
            }
            s_alert_temp_thresh = val;
            config_save_i32("alert_temp", (int32_t)(val * 10.0f));
            snprintf(reply, reply_size, "OK ALERT TEMP %.1f", val);
            return strlen(reply);
        }
        if (strncmp(arg, "HEAP ", 5) == 0) {
            int val = atoi(arg + 5);
            if (val < 0 || val > 300000) {
                snprintf(reply, reply_size, "ERR ALERT HEAP range 0-300000 (0=off)");
                return strlen(reply);
            }
            s_alert_heap_thresh = (uint32_t)val;
            config_save_i32("alert_heap", val);
            snprintf(reply, reply_size, "OK ALERT HEAP %d", val);
            return strlen(reply);
        }
        if (strcmp(arg, "OFF") == 0) {
            s_alert_temp_thresh = 0.0f;
            s_alert_heap_thresh = 0;
            config_save_i32("alert_temp", 0);
            config_save_i32("alert_heap", 0);
            snprintf(reply, reply_size, "OK ALERT all disabled");
            return strlen(reply);
        }
        snprintf(reply, reply_size, "ERR ALERT TEMP <c>|HEAP <bytes>|OFF");
        return strlen(reply);
    }

    /* HELP */
    if (strcmp(cmd, "HELP") == 0) {
        int pos = 0;
        pos += snprintf(reply + pos, reply_size - pos,
            "OK HELP\n"
            "STATUS CONFIG PROFILE PING HELP\n"
            "RATE <10-100>  POWER <2-20>  TARGET <ip> [port]\n"
            "HOSTNAME [name]  CSI [ON|OFF]  CSIMODE [RAW|COMPACT|HYBRID N]\n"
            "ADAPTIVE [ON|OFF]  THRESHOLD <0-1>\n"
            "BLE [ON|OFF]  SCANRATE <5-300>  PROBERATE <1-300>\n"
            "CALIBRATE [STATUS|CLEAR|N]  PRESENCE [ON|OFF|THRESHOLD]\n"
            "CHANSCAN [ON|OFF|NOW|INTERVAL]  LED [QUIET|AUTO]\n"
            "POWERSAVE [ON|OFF]  AUTH [secret]  FLOODTHRESH <n> [win]\n"
            "ALERT [TEMP <c>|HEAP <bytes>|OFF]  LOG <level>  RSSI RESET\n"
            "OTA <url>  POWERTEST [dwell]  IDENTIFY  REBOOT  FACTORY");
        return pos;
    }

    /* FACTORY — erase all NVS config and reboot */
    if (strcmp(cmd, "FACTORY") == 0) {
        snprintf(reply, reply_size, "OK FACTORY erasing config and rebooting");
        /* Send reply before erasing */
        nvs_handle_t fh;
        if (nvs_open("csi_config", NVS_READWRITE, &fh) == ESP_OK) {
            nvs_erase_all(fh);
            nvs_commit(fh);
            nvs_close(fh);
        }
        xTaskCreate(reboot_after_delay, "reboot", 1024, NULL, 1, NULL);
        return strlen(reply);
    }

    /* CONFIG — dump all NVS settings */
    if (strcmp(cmd, "CONFIG") == 0) {
        const char *csi_mode_str = (s_csi_mode == CSI_MODE_COMPACT) ? "compact" :
                                   (s_csi_mode == CSI_MODE_HYBRID)  ? "hybrid" : "raw";
        int pos = 0;
        pos += snprintf(reply + pos, reply_size - pos,
            "OK CONFIG\n"
            "hostname=%s\n"
            "send_rate=%d\n"
            "tx_power=%d\n"
            "target=%s:%d\n"
            "csi=%s\n"
            "csi_mode=%s\n"
            "hybrid_n=%d\n"
            "adaptive=%s\n"
            "threshold=%.6f\n"
            "ble=%s\n"
            "scan_rate=%ds\n"
            "probe_rate=%ds\n"
            "presence=%s\n"
            "pr_thresh=%.4f\n"
            "baseline_nsub=%d\n"
            "chanscan=%s\n"
            "chanscan_int=%ds\n"
            "led=%s\n"
            "powersave=%s\n"
            "auth=%s\n"
            "flood_thresh=%d/%ds\n"
            "alert_temp=%.1f\n"
            "alert_heap=%lu\n"
            "boots=%lu",
            s_hostname,
            s_send_frequency,
            (int)s_tx_power_dbm,
            s_target_ip, s_target_port,
            s_csi_enabled ? "on" : "off",
            csi_mode_str,
            s_hybrid_interval,
            s_adaptive ? "on" : "off",
            s_motion_threshold,
            s_ble_enabled ? "on" : "off",
            (int)(s_ble_scan_interval_us / 1000000LL),
            (int)(s_probe_dedup_us / 1000000LL),
            s_presence_enabled ? "on" : "off",
            s_pr_threshold,
            s_baseline_nsub,
            s_chanscan_enabled ? "on" : "off",
            s_chanscan_interval_s,
            s_led_quiet ? "quiet" : "auto",
            s_powersave ? "on" : "off",
            s_auth_secret[0] ? "on" : "off",
            s_flood_thresh, s_flood_window_s,
            s_alert_temp_thresh, (unsigned long)s_alert_heap_thresh,
            (unsigned long)s_boot_count);
        return pos;
    }

    snprintf(reply, reply_size, "ERR UNKNOWN");
    return strlen(reply);
}

static void cmd_task(void *arg)
{
    int sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
    if (sock < 0) {
        ESP_LOGE(TAG, "cmd_task: socket failed: errno %d", errno);
        vTaskDelete(NULL);
        return;
    }

    struct sockaddr_in bind_addr = {
        .sin_family = AF_INET,
        .sin_port = htons(CONFIG_CSI_CMD_PORT),
        .sin_addr.s_addr = htonl(INADDR_ANY),
    };

    if (bind(sock, (struct sockaddr *)&bind_addr, sizeof(bind_addr)) < 0) {
        ESP_LOGE(TAG, "cmd_task: bind failed: errno %d", errno);
        close(sock);
        vTaskDelete(NULL);
        return;
    }

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

    char rx_buf[192];
    char reply_buf[1400];
    struct sockaddr_in src_addr;
    socklen_t src_len;

    while (1) {
        src_len = sizeof(src_addr);
        int len = recvfrom(sock, rx_buf, sizeof(rx_buf) - 1, 0,
                           (struct sockaddr *)&src_addr, &src_len);
        if (len < 0) {
            ESP_LOGE(TAG, "cmd_task: recvfrom error: errno %d", errno);
            vTaskDelay(pdMS_TO_TICKS(1000));
            continue;
        }

        /* Strip trailing whitespace */
        while (len > 0 && (rx_buf[len - 1] == '\n' || rx_buf[len - 1] == '\r' || rx_buf[len - 1] == ' ')) {
            len--;
        }
        rx_buf[len] = '\0';

        /* Log command (redact HMAC token) */
        if (strncmp(rx_buf, "HMAC:", 5) == 0 && strlen(rx_buf) > 22) {
            ESP_LOGI(TAG, "CMD rx: HMAC:****:%s", rx_buf + 22);
        } else {
            ESP_LOGI(TAG, "CMD rx: \"%s\"", rx_buf);
        }

        /* Authenticate: HMAC grants full access; plain commands are read-only */
        const char *cmd = rx_buf;
        bool authed = false;
        int reply_len;

        if (strncmp(rx_buf, "HMAC:", 5) == 0) {
            cmd = auth_verify(rx_buf, reply_buf, sizeof(reply_buf));
            if (cmd) {
                authed = true;
            }
        } else if (s_auth_secret[0] == '\0') {
            authed = true;
        }

        if (!cmd) {
            /* HMAC verification failed — error set by auth_verify */
            reply_len = strlen(reply_buf);
        } else if (!authed && cmd_requires_auth(cmd)) {
            reply_len = snprintf(reply_buf, sizeof(reply_buf), "ERR AUTH required");
        } else {
            reply_len = cmd_handle(cmd, reply_buf, sizeof(reply_buf));
        }
        sendto(sock, reply_buf, reply_len, 0,
               (struct sockaddr *)&src_addr, src_len);

        ESP_LOGI(TAG, "CMD tx: \"%s\"", reply_buf);
    }
}

/* --- WiFi event handler --- */

static void wifi_event_handler(void *arg, esp_event_base_t event_base,
                               int32_t event_id, void *event_data)
{
    if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) {
        s_wifi_connected = false;
        ESP_LOGW(TAG, "WiFi disconnected");
    }
    if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
        bool was_disconnected = !s_wifi_connected;
        s_wifi_connected = true;

        if (was_disconnected && s_udp_socket >= 0) {
            ip_event_got_ip_t *event = (ip_event_got_ip_t *)event_data;
            wifi_ap_record_t ap;
            int rssi = 0;
            if (esp_wifi_sta_get_ap_info(&ap) == ESP_OK) {
                rssi = ap.rssi;
            }
            char evt[128];
            int len = snprintf(evt, sizeof(evt),
                "EVENT,%s,wifi=reconnected rssi=%d ip=" IPSTR "\n",
                s_hostname, rssi, IP2STR(&event->ip_info.ip));
            sendto(s_udp_socket, evt, len, 0,
                (struct sockaddr *)&s_dest_addr, sizeof(s_dest_addr));
            ESP_LOGI(TAG, "WiFi reconnected, event sent");
        }
    }
}

/* --- Main --- */

void app_main()
{
    ESP_ERROR_CHECK(nvs_flash_init());
    config_load_nvs();

    led_gpio_init();
    xTaskCreate(led_task, "led_task", 2048, NULL, 2, NULL);

    ESP_ERROR_CHECK(esp_netif_init());
    ESP_ERROR_CHECK(esp_event_loop_create_default());

    /* Register WiFi event handler for reconnect notifications */
    ESP_ERROR_CHECK(esp_event_handler_instance_register(
        WIFI_EVENT, WIFI_EVENT_STA_DISCONNECTED, wifi_event_handler, NULL, NULL));
    ESP_ERROR_CHECK(esp_event_handler_instance_register(
        IP_EVENT, IP_EVENT_STA_GOT_IP, wifi_event_handler, NULL, NULL));

    /**
     * @brief This helper function configures Wi-Fi, as selected in menuconfig.
     *        Read "Establishing Wi-Fi Connection" section in esp-idf/examples/protocols/README.md
     *        for more information about this function.
     */
    ESP_ERROR_CHECK(example_connect());
    s_wifi_connected = true;

    /* Apply saved TX power after WiFi is up */
    esp_wifi_set_max_tx_power(s_tx_power_dbm * 4);
    ESP_LOGI(TAG, "TX power set to %d dBm", (int)s_tx_power_dbm);

    /* Power management: DFS (240→80 MHz) + light sleep */
    esp_pm_config_t pm_config = {
        .max_freq_mhz = 240,
        .min_freq_mhz = 80,
        .light_sleep_enable = true,
    };
    esp_err_t pm_err = esp_pm_configure(&pm_config);
    if (pm_err == ESP_OK) {
        ESP_LOGI(TAG, "PM: DFS 240/80 MHz, light sleep enabled");
    } else {
        ESP_LOGW(TAG, "PM configure failed: %s", esp_err_to_name(pm_err));
    }

    /* Apply saved WiFi power save mode */
    if (s_powersave) {
        esp_wifi_set_ps(WIFI_PS_MIN_MODEM);
        ESP_LOGI(TAG, "WiFi modem sleep enabled");
    } else {
        esp_wifi_set_ps(WIFI_PS_NONE);
    }

    /* Chip temperature sensor (ESP32-S2/S3/C3/C6 only) */
#if SOC_TEMP_SENSOR_SUPPORTED
    temperature_sensor_config_t temp_cfg = TEMPERATURE_SENSOR_CONFIG_DEFAULT(-10, 80);
    if (temperature_sensor_install(&temp_cfg, &s_temp_handle) == ESP_OK) {
        temperature_sensor_enable(s_temp_handle);
        ESP_LOGI(TAG, "Temperature sensor initialized");
    } else {
        ESP_LOGW(TAG, "Temperature sensor init failed");
        s_temp_handle = NULL;
    }
#endif

    /* mDNS: announce as <hostname>.local with _esp-csi._udp service */
    ESP_ERROR_CHECK(mdns_init());
    mdns_hostname_set(s_hostname);
    mdns_instance_name_set("ESP32 CSI Sensor");
    mdns_service_add(NULL, "_esp-csi", "_udp", s_target_port, NULL, 0);
    ESP_LOGI(TAG, "mDNS hostname: %s.local (_esp-csi._udp:%d)", s_hostname, s_target_port);

    /* Watchdog: 30s timeout, auto-reboot on hang */
    esp_task_wdt_config_t wdt_cfg = {
        .timeout_ms = 30000,
        .idle_core_mask = (1 << 0) | (1 << 1),
        .trigger_panic = true,
    };
    ESP_ERROR_CHECK(esp_task_wdt_reconfigure(&wdt_cfg));
    ESP_LOGI(TAG, "Watchdog configured: 30s timeout");

    /* BLE: Initialize NimBLE stack */
    ESP_ERROR_CHECK(nimble_port_init());
    ble_hs_cfg.reset_cb = ble_on_reset;
    ble_hs_cfg.sync_cb = ble_on_sync;
    nimble_port_freertos_init(ble_host_task);

    /* BLE: periodic scan restart to refresh duplicate filter */
    const esp_timer_create_args_t ble_timer_args = {
        .callback = ble_scan_restart_timer_cb,
        .name = "ble_scan",
    };
    esp_timer_create(&ble_timer_args, &s_ble_timer);
    esp_timer_start_periodic(s_ble_timer, s_ble_scan_interval_us);

    ESP_LOGI(TAG, "BLE: NimBLE initialized, scan=%s", s_ble_enabled ? "on" : "off");

    s_led_mode = s_led_quiet ? LED_OFF : LED_SLOW_BLINK;

    udp_socket_init();
    wifi_csi_init();
#if !CONFIG_IDF_TARGET_ESP32
    /* Promiscuous mode disables CSI on original ESP32 — only enable on newer chips */
    wifi_promiscuous_init();
#endif
    wifi_ping_router_start();

    xTaskCreate(cmd_task, "cmd_task", 6144, NULL, 5, NULL);
    xTaskCreate(adaptive_task, "adaptive", 3072, NULL, 3, NULL);

    /* OTA rollback: mark firmware valid if we got this far */
    const esp_partition_t *running = esp_ota_get_running_partition();
    esp_ota_img_states_t ota_state;
    if (esp_ota_get_state_partition(running, &ota_state) == ESP_OK) {
        if (ota_state == ESP_OTA_IMG_PENDING_VERIFY) {
            ESP_LOGI(TAG, "OTA: marking firmware valid (rollback cancelled)");
            esp_ota_mark_app_valid_cancel_rollback();
        }
    }
}
