// ============================================================================
// mqtt_client.cpp
// ============================================================================

#include "mqtt_client.h"
#include "command_handler.h"
#include "sensor_location.h"   // location_resolve_report() — shared position source
#include "mesh_timesync.h"     // timesync_resolve_report_ts() — shared time source
#include "gps_module.h"        // gps_get_stats() for the status heartbeat
#include "c2_wifi_bridge.h"    // C2 heartbeat health, surfaced over MQTT too
#include "config.h"            // PDL_BOARD_TYPE
#include <cmath>               // llround

#include <cstring>
#include <cstdio>

#ifdef ARDUINO
#include <Arduino.h>
#include <WiFi.h>
#include <WiFiClient.h>
#include <WiFiClientSecure.h>
#include <PubSubClient.h>
#include <nvs_flash.h>
#include <nvs.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
extern "C" {
#include "esp_wifi.h"
}
#endif

static const char* TAG_MQ = "MQTT";

// ---------------------------------------------------------------------------
// Static config + state
// ---------------------------------------------------------------------------
static char     s_ssid[33]        = {0};
static char     s_psk[65]         = {0};
static char     s_broker[64]      = {0};
static uint16_t s_port            = MQTT_DEFAULT_PORT;
static char     s_client_id[40]   = {0};
static bool     s_use_tls         = false;
static char     s_user[33]        = {0};
static char     s_pass[33]        = {0};

static bool     s_configured      = false;
static bool     s_mqtt_initialised     = false;
static MqttStats s_mqtt_stats;

#ifdef ARDUINO
static WiFiClient        s_plain;
static WiFiClientSecure  s_tls_client;
static PubSubClient      s_mqtt(s_plain);
static TaskHandle_t      s_task = nullptr;
#endif

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
size_t mqtt_format_topic(char* out, size_t out_len,
                         const char* sensor_name, const char* suffix) {
    if (!out || out_len == 0) return 0;
    int n = snprintf(out, out_len, "pdl/%s/%s", sensor_name, suffix);
    if (n < 0) { out[0] = 0; return 0; }
    if ((size_t)n >= out_len) { out[out_len - 1] = 0; return out_len - 1; }
    return (size_t)n;
}

size_t mqtt_format_detection_json(const DetectionEvent& evt,
                                  char* out, size_t out_len) {
    if (!out || out_len == 0) return 0;

    // Resolve position through the SAME helper the C2 path uses. Previously
    // this published evt.lat_fixed/lon_fixed raw, so a relayed peer event —
    // whose own coordinates are populated by the relay, not the packet —
    // reached C2 with a real position but MQTT with 0,0. One resolver now
    // serves C2, MQTT and any future Reticulum/rmap announce.
    //
    // Values stay fixed-point (degrees * 1e7) on the wire so existing MQTT
    // subscribers are unaffected; only the SOURCE of the numbers changed.
    int32_t lat_out = 0, lon_out = 0;
    double  lat_deg = 0.0, lon_deg = 0.0;
    if (location_resolve_report(evt.lat_fixed, evt.lon_fixed, &lat_deg, &lon_deg)) {
        lat_out = (int32_t)llround(lat_deg * 1e7);
        lon_out = (int32_t)llround(lon_deg * 1e7);
    }

    // Resolve time through the SAME helper C2 uses. Publishing
    // evt.timestamp_utc raw meant MQTT carried boot-seconds while C2 carried
    // real UTC for the identical detection (measured: MQTT ts=109 vs C2
    // ts=1785668807).
    uint32_t ts_out = timesync_resolve_report_ts(evt.timestamp_utc);

    int n = snprintf(
        out, out_len,
        "{\"sid\":%u,\"ts\":%u,\"cls\":%u,\"conf\":%u,\"rssi\":%d,\"nf\":%d,"
        "\"lat\":%d,\"lon\":%d,\"batt\":%u,\"flags\":%u}",
        (unsigned)evt.sensor_id,
        (unsigned)ts_out,
        (unsigned)evt.class_label,
        (unsigned)evt.confidence,
        (int)evt.rssi,
        (int)evt.noise_floor,
        (int)lat_out,
        (int)lon_out,
        (unsigned)evt.battery_pct,
        (unsigned)evt.flags
    );
    if (n < 0) { out[0] = 0; return 0; }
    if ((size_t)n >= out_len) { out[out_len - 1] = 0; return out_len - 1; }
    return (size_t)n;
}

bool mqtt_is_configured() { return s_configured; }

void mqtt_get_stats(MqttStats* out) { if (out) *out = s_mqtt_stats; }

// ---------------------------------------------------------------------------
// NVS load
// ---------------------------------------------------------------------------
#ifdef ARDUINO
static void load_nvs_config() {
    nvs_handle_t h;
    if (nvs_open(MQTT_NVS_NS, NVS_READONLY, &h) != ESP_OK) return;

    size_t sl = sizeof(s_ssid);
    if (nvs_get_str(h, MQTT_NVS_SSID, s_ssid, &sl) != ESP_OK) s_ssid[0] = 0;
    sl = sizeof(s_psk);
    if (nvs_get_str(h, MQTT_NVS_PSK, s_psk, &sl) != ESP_OK) s_psk[0] = 0;
    sl = sizeof(s_broker);
    if (nvs_get_str(h, MQTT_NVS_BROKER, s_broker, &sl) != ESP_OK) s_broker[0] = 0;

    uint16_t pv = 0;
    if (nvs_get_u16(h, MQTT_NVS_PORT, &pv) == ESP_OK && pv != 0) s_port = pv;

    sl = sizeof(s_client_id);
    if (nvs_get_str(h, MQTT_NVS_CLIENT, s_client_id, &sl) != ESP_OK) {
        snprintf(s_client_id, sizeof(s_client_id), "pdl-%s", SENSOR_NAME);
    }

    uint8_t t = 0;
    if (nvs_get_u8(h, MQTT_NVS_TLS, &t) == ESP_OK && t == 1) {
        s_use_tls = true;
        if (s_port == MQTT_DEFAULT_PORT) s_port = MQTT_DEFAULT_TLS_PORT;
    }

    sl = sizeof(s_user);
    nvs_get_str(h, MQTT_NVS_USER, s_user, &sl);
    sl = sizeof(s_pass);
    nvs_get_str(h, MQTT_NVS_PASS, s_pass, &sl);

    nvs_close(h);

    // Gate on the BROKER only.
    //
    // This previously also required s_ssid, from when mqtt_client managed its
    // own WiFi association. It no longer needs to: wifi_status_server's
    // autojoin brings the station up long before MQTT is reached, and
    // ensure_wifi() below short-circuits when WiFi is already connected.
    //
    // The old gate meant configuring a broker without ALSO duplicating the
    // WiFi credentials into this second NVS namespace left s_configured false
    // — mqtt_task() then idled at 60s sleeps and nothing was ever published,
    // with no error anywhere. An optional s_ssid is still honoured as a
    // fallback for a unit that has no other WiFi management.
    s_configured = (strlen(s_broker) > 0);
}

// ---------------------------------------------------------------------------
// PubSubClient callback — receives MQTT messages on subscribed topics.
// We only subscribe to one topic (pdl/<sensor_name>/cmd) so any callback
// here is treated as a CMD_PACKET and handed verbatim to the existing
// command_handler.
// ---------------------------------------------------------------------------
static void mqtt_message_cb(char* topic, uint8_t* payload, unsigned int len) {
    s_mqtt_stats.cmd_received++;
    Serial.printf("[%s] CMD via MQTT topic=%s len=%u\n", TAG_MQ, topic, len);
    handleIncomingCommand(payload, len);
}

// ---------------------------------------------------------------------------
// Lock the WiFi promiscuous sniffer to the AP's channel once we associate,
// so the existing wifi_inject / sniff loops stay synchronised.
// ---------------------------------------------------------------------------
static void pin_promisc_to_ap_channel() {
    if (WiFi.status() != WL_CONNECTED) return;
    int ch = WiFi.channel();
    if (ch <= 0) return;
    esp_wifi_set_channel((uint8_t)ch, WIFI_SECOND_CHAN_NONE);
    Serial.printf("[%s] promisc pinned to AP channel %d\n", TAG_MQ, ch);
}

static bool ensure_wifi() {
    if (WiFi.status() == WL_CONNECTED) return true;

    // No MQTT-specific credentials: this unit's WiFi is owned by
    // wifi_status_server / autojoin, so just report not-ready and let the
    // task's backoff retry. Calling WiFi.begin("","") here would tear down a
    // perfectly good association attempt in progress elsewhere.
    if (strlen(s_ssid) == 0) return false;

    Serial.printf("[%s] connecting WiFi ssid=%s\n", TAG_MQ, s_ssid);
    WiFi.mode(WIFI_STA);
    WiFi.begin(s_ssid, s_psk);
    uint32_t deadline = millis() + 15000;
    while (WiFi.status() != WL_CONNECTED && (int32_t)(millis() - deadline) < 0) {
        vTaskDelay(pdMS_TO_TICKS(200));
    }
    if (WiFi.status() != WL_CONNECTED) {
        Serial.printf("[%s] WiFi connect failed\n", TAG_MQ);
        return false;
    }
    Serial.printf("[%s] WiFi up: %s ch=%d\n", TAG_MQ,
                  WiFi.localIP().toString().c_str(), WiFi.channel());
    pin_promisc_to_ap_channel();
    return true;
}

static bool ensure_mqtt() {
    if (s_mqtt.connected()) return true;
    if (!ensure_wifi()) return false;

    if (s_use_tls) {
        s_tls_client.setInsecure();   // CA wire-up is a follow-up; see notes
        s_mqtt.setClient(s_tls_client);
    } else {
        s_mqtt.setClient(s_plain);
    }
    s_mqtt.setServer(s_broker, s_port);
    s_mqtt.setBufferSize(MQTT_MAX_PACKET_SIZE);
    s_mqtt.setCallback(mqtt_message_cb);

    bool ok;
    if (strlen(s_user) > 0) {
        ok = s_mqtt.connect(s_client_id, s_user, s_pass);
    } else {
        ok = s_mqtt.connect(s_client_id);
    }
    if (!ok) {
        Serial.printf("[%s] connect failed rc=%d\n", TAG_MQ, s_mqtt.state());
        s_mqtt_stats.connect_fail++;
        return false;
    }
    s_mqtt_stats.connect_ok++;

    char topic[64];
    mqtt_format_topic(topic, sizeof(topic), SENSOR_NAME, "cmd");
    s_mqtt.subscribe(topic, 1);
    Serial.printf("[%s] connected, subscribed %s\n", TAG_MQ, topic);
    return true;
}

// ---------------------------------------------------------------------------
// Background task — loops PubSubClient, attempts reconnect with backoff
// ---------------------------------------------------------------------------
static void mqtt_task(void* arg) {
    (void)arg;
    uint32_t backoff_ms = 1000;
    for (;;) {
        if (!s_configured) {
            vTaskDelay(pdMS_TO_TICKS(60000));
            continue;
        }
        if (!ensure_mqtt()) {
            vTaskDelay(pdMS_TO_TICKS(backoff_ms));
            backoff_ms = backoff_ms < 30000 ? backoff_ms * 2 : 30000;
            continue;
        }
        backoff_ms = 1000;
        s_mqtt.loop();
        vTaskDelay(pdMS_TO_TICKS(50));
    }
}
#endif  // ARDUINO

// ---------------------------------------------------------------------------
// Public lifecycle
// ---------------------------------------------------------------------------
void mqtt_init() {
    memset(&s_mqtt_stats, 0, sizeof(s_mqtt_stats));
    s_mqtt_initialised = true;

#ifdef ARDUINO
    load_nvs_config();
    if (!s_configured) {
        Serial.printf("[%s] not configured, MQTT disabled\n", TAG_MQ);
        return;
    }
    Serial.printf("[%s] init broker=%s:%u tls=%d client=%s\n",
                  TAG_MQ, s_broker, s_port, s_use_tls ? 1 : 0, s_client_id);
    xTaskCreatePinnedToCore(mqtt_task, "pdl_mqtt", 6144, nullptr,
                            tskIDLE_PRIORITY + 1, &s_task, 0);
#else
    s_configured = false;  // native build: never live
#endif
}

bool mqtt_is_connected() {
#ifdef ARDUINO
    return s_mqtt_initialised && s_configured && s_mqtt.connected();
#else
    return false;
#endif
}

bool mqtt_publish_detection(const DetectionEvent& evt) {
    if (!mqtt_is_connected()) return false;
#ifdef ARDUINO
    char topic[64];
    char body[256];
    mqtt_format_topic(topic, sizeof(topic), SENSOR_NAME, "detection");
    size_t blen = mqtt_format_detection_json(evt, body, sizeof(body));
    bool ok = s_mqtt.publish(topic, (uint8_t*)body, blen, false);
    if (ok) s_mqtt_stats.pub_detection++; else s_mqtt_stats.pub_failures++;
    return ok;
#else
    (void)evt;
    return false;
#endif
}

// ---------------------------------------------------------------------------
// Periodic status heartbeat.
//
// WHY THIS EXISTS
// ---------------
// Before this, MQTT traffic was produced ONLY as a side effect of activity —
// a VQC score or a detection. A perfectly healthy unit with nothing to report
// was indistinguishable on the broker from one that had died. Observed live:
// Gateway01 and Spencer03 were connected, posting to C2 and relaying LoRa, yet
// published nothing for a full 55s window while four peers did.
//
// For an operational fleet that is the wrong default. Absence of a heartbeat
// must MEAN something. Every unit now emits pdl/<name>/status on a fixed
// cadence regardless of activity, carrying the same health fields the HTTP
// /status endpoint exposes — so the broker alone is sufficient to monitor the
// fleet, without polling six IP addresses that may have changed by DHCP.
// ---------------------------------------------------------------------------
void mqtt_tick() {
#ifdef ARDUINO
    if (!mqtt_is_connected()) return;

    static uint32_t s_last_status_ms = 0;
    uint32_t now = millis();
    if (s_last_status_ms != 0 && (now - s_last_status_ms) < MQTT_STATUS_INTERVAL_MS) {
        return;
    }
    s_last_status_ms = now;

    GpsStats g{};
    gps_get_stats(&g);

    int32_t lat_fx = 0, lon_fx = 0;
    location_get(&lat_fx, &lon_fx);

    uint32_t c2_since = c2_wifi_bridge_seconds_since_success();

    char body[384];
    snprintf(body, sizeof(body),
        // fw + build stamp travel in every heartbeat so firmware divergence is
        // visible fleet-wide from the broker alone. Tonight three separate
        // problems were traced to units silently running different images with
        // no way to tell them apart; the only symptom was which bug they still
        // exhibited.
        "{\"sensor\":\"%s\",\"board\":\"" PDL_BOARD_TYPE "\","
        "\"fw\":\"" SENSOR_FW_VERSION "\",\"build\":\"" PDL_BUILD_STAMP "\","
        "\"uptime_s\":%lu,"
        "\"rssi\":%d,\"lat\":%.7f,\"lon\":%.7f,"
        "\"gps_fix\":%s,\"gps_sats\":%u,"
        "\"c2_secs_since_success\":%lu,\"c2_fail\":%lu,\"c2_stalls\":%lu,"
        "\"free_heap\":%lu}",
        SENSOR_NAME, (unsigned long)(now / 1000),
        (int)WiFi.RSSI(),
        (double)lat_fx / 1e7, (double)lon_fx / 1e7,
        g.has_fix ? "true" : "false", (unsigned)g.satellites,
        (unsigned long)c2_since,
        (unsigned long)c2_wifi_bridge_consecutive_failures(),
        (unsigned long)c2_wifi_bridge_stall_recoveries(),
        (unsigned long)ESP.getFreeHeap());

    mqtt_publish_text("status", body);
#endif
}

bool mqtt_publish_text(const char* topic_suffix, const char* json_payload) {
    if (!mqtt_is_connected()) return false;
#ifdef ARDUINO
    char topic[64];
    mqtt_format_topic(topic, sizeof(topic), SENSOR_NAME, topic_suffix);
    bool retain = (strcmp(topic_suffix, "alert") == 0);
    bool ok = s_mqtt.publish(topic, json_payload, retain);
    if (ok) s_mqtt_stats.pub_text++; else s_mqtt_stats.pub_failures++;
    return ok;
#else
    (void)topic_suffix; (void)json_payload;
    return false;
#endif
}
