#include "ffps.h"
#include "mesh_publish.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 by network_id.cpp
// / threat_correlator.cpp / blue_force.cpp. millis() is test-overridable
// via STUB_MILLIS_PROVIDED (the corroboration window is time-based).
#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;
#endif // ARDUINO

static const char* TAG = "FFPS";

static uint32_t     s_sensor_id = 0;
static FFPSState    s_state = FFPS_IDLE;
static DetectionEvent s_candidate;
static uint8_t      s_corroboration_count = 0;
static uint32_t     s_window_start_ms = 0;

// Hash of the current candidate for matching corroboration responses
static uint8_t      s_active_hash[8];

// BUG FIX (found while writing this module's first tests): corroboration
// counting had no dedup by sensor_id -- a single peer's CORROBORATE packet
// getting duplicated (a common LoRa mesh occurrence: multi-hop relay
// rebroadcast, a retried TX, etc.) would increment s_corroboration_count
// once per delivery, not once per genuinely independent corroborating
// sensor. With FFPS_MIN_CORROBORATORS=1 today this has no observable
// effect (one real corroboration already satisfies K=1 on its own), but it
// would silently defeat the entire point of "federated" false-positive
// suppression the moment K is ever raised above 1 -- a single peer's
// retransmitted packet could satisfy a K=2+ threshold without any second
// independent sensor ever actually agreeing. Track which sensor_ids have
// already been counted for the current candidate and ignore repeats.
#define FFPS_MAX_TRACKED_CORROBORATORS 8
static uint32_t s_corroborator_ids[FFPS_MAX_TRACKED_CORROBORATORS];
static uint8_t  s_corroborator_id_count = 0;

static bool already_corroborated(uint32_t sensor_id) {
    for (uint8_t i = 0; i < s_corroborator_id_count; i++) {
        if (s_corroborator_ids[i] == sensor_id) return true;
    }
    return false;
}

// ---------------------------------------------------------------------------
void ffps_init(uint32_t sensor_id) {
    s_sensor_id = sensor_id;
    s_state = FFPS_IDLE;
    s_corroboration_count = 0;
    Serial.printf("[%s] Initialized. K=%d, window=%d ms\n",
                  TAG, FFPS_MIN_CORROBORATORS, FFPS_CORROBORATION_WINDOW_MS);
}

// ---------------------------------------------------------------------------
// Classes whose content IS the evidence (a decoded broadcast message), not a
// probabilistic inference over one sensor's own RF/spectral observation.
// FFPS's multi-sensor corroboration exists to catch a single sensor's own
// classifier hallucinating on ambient noise — it has nothing to check a
// decoded ODID serial/GPS broadcast against; requiring a second sensor to
// "agree" before publishing just delays (and on a solo deployment,
// silently drops) a detection that was never uncertain in the first place.
static bool isDirectDecodeClass(uint8_t class_label) {
    return class_label == CLASS_RID_BLE_DECODED ||
           class_label == CLASS_RID_WIFI_DECODED;
}

bool ffps_submit_candidate(const DetectionEvent& candidate) {
    if (s_state != FFPS_IDLE) {
        Serial.printf("[%s] Already in corroboration, ignoring new candidate\n", TAG);
        return false;
    }

    if (isDirectDecodeClass(candidate.class_label)) {
        s_candidate = candidate;
        s_candidate.msg_type = MSG_TYPE_DETECTION;
        s_candidate.flags = FLAG_PROMOTED;
        s_state = FFPS_PROMOTED;
        Serial.printf("[%s] class=%d is a direct decode (not an inference) — "
                      "promoting without corroboration\n", TAG, candidate.class_label);
        return true;
    }

    // Store candidate
    s_candidate = candidate;
    s_candidate.msg_type = MSG_TYPE_CANDIDATE;
    s_candidate.flags = 0;  // clear flags for candidate phase
    memcpy(s_active_hash, candidate.signature_hash, 8);

    // Broadcast candidate on mesh
    uint8_t pkt[DETECTION_PACKET_SIZE];
    size_t len = mesh_serialize_event(s_candidate, pkt, sizeof(pkt));
    if (len == 0 || !mesh_publish_raw(pkt, len)) {
        Serial.printf("[%s] Failed to broadcast candidate\n", TAG);
        return false;
    }

    // Start corroboration window
    s_state = FFPS_WAITING_CORROBORATION;
    s_corroboration_count = 0;
    s_corroborator_id_count = 0;
    s_window_start_ms = millis();

    // Return to receive mode so we can hear corroboration replies
    mesh_start_receive();

    Serial.printf("[%s] Candidate broadcast. class=%d conf=%d, waiting for corroboration\n",
                  TAG, candidate.class_label, candidate.confidence);
    return true;
}

// ---------------------------------------------------------------------------
void ffps_handle_packet(const uint8_t* data, size_t len, float rssi) {
    if (len < 1) return;

    uint8_t msg_type = data[0];

    if (msg_type == MSG_TYPE_CANDIDATE && s_state == FFPS_IDLE) {
        // Another sensor's candidate — check if we have a matching observation
        // and send corroboration if so.
        DetectionEvent incoming;
        if (!mesh_deserialize_event(data, len, incoming)) return;

        // Skip our own candidates (shouldn't happen due to timing, but guard)
        if (incoming.sensor_id == s_sensor_id) return;

        // For now, we corroborate if the class label matches any recent
        // non-noise detection we had. A more sophisticated version would
        // compare signature hashes and timing.
        // Simple policy: always corroborate non-noise candidates from peers
        // (the mesh consensus will self-regulate).
        if (incoming.class_label != CLASS_NOISE) {
            // Build corroboration reply
            DetectionEvent reply;
            memset(&reply, 0, sizeof(reply));
            reply.msg_type = MSG_TYPE_CORROBORATE;
            reply.sensor_id = s_sensor_id;
            reply.timestamp_utc = incoming.timestamp_utc;
            reply.class_label = incoming.class_label;
            reply.confidence = incoming.confidence;
            memcpy(reply.signature_hash, incoming.signature_hash, 8);

            uint8_t pkt[DETECTION_PACKET_SIZE];
            size_t pkt_len = mesh_serialize_event(reply, pkt, sizeof(pkt));
            if (pkt_len > 0) {
                mesh_publish_raw(pkt, pkt_len);
                mesh_start_receive();
                Serial.printf("[%s] Sent corroboration for peer sensor 0x%08X class=%d\n",
                              TAG, incoming.sensor_id, incoming.class_label);
            }
        }
    }
    else if (msg_type == MSG_TYPE_CORROBORATE && s_state == FFPS_WAITING_CORROBORATION) {
        // Corroboration response for our candidate
        DetectionEvent corr;
        if (!mesh_deserialize_event(data, len, corr)) return;

        // Skip our own messages
        if (corr.sensor_id == s_sensor_id) return;

        // Verify signature hash matches our active candidate
        if (memcmp(corr.signature_hash, s_active_hash, 8) == 0) {
            if (already_corroborated(corr.sensor_id)) {
                Serial.printf("[%s] Duplicate corroboration from 0x%08X ignored\n",
                              TAG, corr.sensor_id);
                return;
            }
            if (s_corroborator_id_count < FFPS_MAX_TRACKED_CORROBORATORS) {
                s_corroborator_ids[s_corroborator_id_count++] = corr.sensor_id;
            }
            s_corroboration_count++;
            Serial.printf("[%s] Corroboration from 0x%08X (count=%d/%d)\n",
                          TAG, corr.sensor_id, s_corroboration_count, FFPS_MIN_CORROBORATORS);
        }
    }
}

// ---------------------------------------------------------------------------
FFPSState ffps_tick() {
    if (s_state != FFPS_WAITING_CORROBORATION) {
        return s_state;
    }

    uint32_t elapsed = millis() - s_window_start_ms;

    // Early promotion: got enough corroborators before window expires
    if (s_corroboration_count >= FFPS_MIN_CORROBORATORS) {
        s_candidate.msg_type = MSG_TYPE_DETECTION;
        s_candidate.flags |= FLAG_PROMOTED;
        s_state = FFPS_PROMOTED;
        Serial.printf("[%s] PROMOTED after %lu ms (%d corroborators)\n",
                      TAG, elapsed, s_corroboration_count);
        return s_state;
    }

    // Window expired. The early-promotion check above already returned for
    // any count >= FFPS_MIN_CORROBORATORS, so by construction the count is
    // still below threshold here -- this is always a suppression, not a
    // second promotion path (a prior version had a redundant, unreachable
    // "promote at expiry" branch here; removed rather than left as dead
    // code implying two distinct promotion routes).
    if (elapsed >= FFPS_CORROBORATION_WINDOW_MS) {
        s_candidate.flags |= FLAG_SUPPRESSED;
        s_state = FFPS_SUPPRESSED;
        Serial.printf("[%s] SUPPRESSED — only %d/%d corroborators\n",
                      TAG, s_corroboration_count, FFPS_MIN_CORROBORATORS);
    }

    return s_state;
}

// ---------------------------------------------------------------------------
const DetectionEvent& ffps_get_promoted_event() {
    return s_candidate;
}

uint8_t ffps_get_corroboration_count() {
    return s_corroboration_count;
}

void ffps_reset() {
    s_state = FFPS_IDLE;
    s_corroboration_count = 0;
    s_corroborator_id_count = 0;
    memset(s_active_hash, 0, sizeof(s_active_hash));
}
