#include "health_report.h"
#include "mesh_publish.h"
#include "model_manager.h"
#include "sensor_location.h"
#include <cstring>

#ifdef ARDUINO
#include <Arduino.h>
#else
// Host-side stub so this module (previously buildable ONLY on-device) can
// be exercised by a native Unity test -- same pattern used throughout this
// sweep. millis() is test-overridable via STUB_MILLIS_PROVIDED.
#include <cstdio>
#include <cstdarg>
#ifndef STUB_MILLIS_PROVIDED
static uint32_t stub_millis_val = 1000;
static inline uint32_t millis() { return stub_millis_val; }
#endif
struct NativeSerialStub {
    void printf(const char* fmt, ...) {
        va_list args;
        va_start(args, fmt);
        vprintf(fmt, args);
        va_end(args);
    }
};
static NativeSerialStub Serial;
// analogRead is test-overridable via STUB_ANALOG_READ_PROVIDED so battery
// tests can control the simulated raw ADC value.
#ifndef STUB_ANALOG_READ_PROVIDED
static inline int analogRead(int) { return 0; }
#endif
#endif // ARDUINO

static const char* TAG = "Health";

static uint32_t s_sensor_id       = 0;
static uint32_t s_last_report_ms  = 0;
static uint32_t s_detection_count = 0;
static uint32_t s_boot_time       = 0;

// RSSI statistics
static int16_t  s_rssi_min    = 0;
static int16_t  s_rssi_max    = -200;
static int32_t  s_rssi_sum    = 0;
static uint32_t s_rssi_count  = 0;

// ---------------------------------------------------------------------------
void health_init(uint32_t sensor_id) {
    s_sensor_id = sensor_id;
    s_last_report_ms = millis();
    s_boot_time = millis();
    s_detection_count = 0;
    s_rssi_min = 0;
    s_rssi_max = -200;
    s_rssi_sum = 0;
    s_rssi_count = 0;
    Serial.printf("[%s] Initialized. Reporting every %d s\n", TAG, HEALTH_REPORT_INTERVAL_MS / 1000);
}

// ---------------------------------------------------------------------------
uint8_t health_battery_pct_from_raw_adc(int raw) {
    // Convert to millivolts (ESP32-S3 ADC range ~0-3.3V, with voltage divider)
    float voltage_mv = (raw / 4095.0f) * 3300.0f * BATT_ADC_DIVIDER;

    // Clamp and map to percentage
    if (voltage_mv >= BATT_FULL_MV) return 100;
    if (voltage_mv <= BATT_EMPTY_MV) return 0;

    return (uint8_t)(((voltage_mv - BATT_EMPTY_MV) / (BATT_FULL_MV - BATT_EMPTY_MV)) * 100.0f);
}

uint8_t health_read_battery_pct() {
    // Read raw ADC value (12-bit, 0-4095)
    int raw = analogRead(BATT_ADC_PIN);
    return health_battery_pct_from_raw_adc(raw);
}

// ---------------------------------------------------------------------------
void health_increment_detections() {
    s_detection_count++;
}

// ---------------------------------------------------------------------------
void health_update_rssi(int16_t rssi) {
    if (rssi < s_rssi_min) s_rssi_min = rssi;
    if (rssi > s_rssi_max) s_rssi_max = rssi;
    s_rssi_sum += rssi;
    s_rssi_count++;
}

// ---------------------------------------------------------------------------
bool health_send_now() {
    // Build health packet using the detection event format
    // We pack health-specific data into the fields:
    //   class_label  = 0 (not applicable)
    //   confidence   = battery_pct
    //   rssi         = average RSSI
    //   noise_floor  = RSSI range (max - min)
    //   signature_hash = [uptime_s(4), detection_count(4)]
    //   lat/lon      = actual position, via location_get() -- same source
    //   c2_wifi_bridge_post_health() already uses for the direct-WiFi path,
    //   so a peer relaying this packet (see main.cpp's MSG_TYPE_HEALTH
    //   case) reports real position instead of (0,0).

    DetectionEvent health;
    memset(&health, 0, sizeof(health));

    health.msg_type      = MSG_TYPE_HEALTH;
    health.sensor_id     = s_sensor_id;
    health.timestamp_utc = (uint32_t)(millis() / 1000);  // TODO: real UTC from NTP/GPS
    health.class_label   = 0;
    health.battery_pct   = health_read_battery_pct();
    health.confidence    = health.battery_pct;  // mirror in confidence field

    // Average RSSI
    if (s_rssi_count > 0) {
        health.rssi = (int16_t)(s_rssi_sum / (int32_t)s_rssi_count);
        // BUG FIX (found while writing this module's first tests): the
        // comment above this function's declaration has always said
        // "noise_floor = RSSI range (max - min)", but this line assigned
        // s_rssi_min directly -- and s_rssi_max was tracked on every
        // health_update_rssi() call yet never actually read anywhere,
        // a strong signal the range calculation was simply incomplete
        // rather than an intentional redefinition. Fixed to match the
        // documented design and finally use the tracked max.
        health.noise_floor = s_rssi_max - s_rssi_min;
    } else {
        health.rssi = 0;
        health.noise_floor = 0;
    }

    // Pack uptime and detection count into signature_hash
    uint32_t uptime_s = (millis() - s_boot_time) / 1000;
    health.signature_hash[0] = (uptime_s >> 24) & 0xFF;
    health.signature_hash[1] = (uptime_s >> 16) & 0xFF;
    health.signature_hash[2] = (uptime_s >> 8)  & 0xFF;
    health.signature_hash[3] = (uptime_s)       & 0xFF;
    health.signature_hash[4] = (s_detection_count >> 24) & 0xFF;
    health.signature_hash[5] = (s_detection_count >> 16) & 0xFF;
    health.signature_hash[6] = (s_detection_count >> 8)  & 0xFF;
    health.signature_hash[7] = (s_detection_count)       & 0xFF;

    location_get(&health.lat_fixed, &health.lon_fixed);
    health.flags = 0;

    bool ok = mesh_publish_detection(health);
    if (ok) {
        Serial.printf("[%s] Health: batt=%d%% uptime=%lu s detections=%lu rssi_avg=%d\n",
                      TAG, health.battery_pct, uptime_s, s_detection_count, health.rssi);

        // Reset RSSI stats for next window
        s_rssi_min = 0;
        s_rssi_max = -200;
        s_rssi_sum = 0;
        s_rssi_count = 0;
    }

    // Return to receive mode after TX
    mesh_start_receive();

    return ok;
}

// ---------------------------------------------------------------------------
void health_print_model_stats() {
    uint8_t count = model_manager_count();
    if (count == 0) return;

    Serial.printf("[%s] === Multi-Model Stats (%d registered, PSRAM %u KB used) ===\n",
                  TAG, count, model_manager_psram_used() / 1024);

    for (uint8_t i = 0; i < MODEL_MAX_SLOTS; i++) {
        ModelStats stats;
        if (!model_manager_get_stats(i, stats)) continue;

        Serial.printf("[%s]   [%d] %-20s %s  pri=%d  infer=%lu  avg_lat=%lu us  model=%lu B  arena=%lu B\n",
                      TAG, stats.slot_id,
                      stats.name,
                      stats.loaded ? "LOADED " : "PAGED  ",
                      stats.priority,
                      stats.inference_count,
                      stats.avg_latency_us,
                      stats.model_size_bytes,
                      stats.arena_used_bytes);
    }
}

// ---------------------------------------------------------------------------
bool health_tick() {
    uint32_t now = millis();
    if ((now - s_last_report_ms) >= HEALTH_REPORT_INTERVAL_MS) {
        s_last_report_ms = now;

        // Print model stats alongside the regular health report
        health_print_model_stats();

        return health_send_now();
    }
    return false;
}
