// ============================================================================
// markov_scorer.h — T1 RID Behavioral Anomaly Scorer (Markov chain)
//
// Models RID event sequences as a first-order Markov chain over
// (event_type, inter-arrival-time-bucket) states. A benign baseline is
// trained server-side and serialised to /models/markov_baseline.bin on
// LittleFS. The sensor loads that binary at boot and computes a per-event
// negative-log-likelihood (NLL) score in a rolling window, surfacing
// anomalies upstream via the existing mesh_publish path.
//
// Memory budget:
//   States     : MK_EVENT_COUNT * MK_IAT_BUCKETS = 7 * 5 = 35
//   Matrix     : 35 * 35 * sizeof(int16_t) = 2450 bytes (~2.4 KB)
//   Track state: 4 tracks * (32 events * 4 bytes + overhead) = ~0.5 KB
//   Total      : < 4 KB SRAM — safe even without PSRAM.
//
// References:
//   Jha, Tan, Maxion "Markov Chains, Classifiers, and Intrusion Detection"
//   (CSFW 2001) — same transition-matrix structure, different domain.
// ============================================================================

#ifndef MARKOV_SCORER_H
#define MARKOV_SCORER_H

#include <cstdint>
#include <cstddef>

// ---------------------------------------------------------------------------
// Event-type vocabulary (must match Python markov_rid.py EventType enum)
// ---------------------------------------------------------------------------
enum MarkovEventType : uint8_t {
    MK_BEACON       = 0,
    MK_HEARTBEAT    = 1,
    MK_TELEMETRY    = 2,
    MK_ID_BROADCAST = 3,
    MK_HANDOFF      = 4,
    MK_LOSS         = 5,
    MK_GAP          = 6,
    MK_EVENT_COUNT  = 7   // sentinel — number of valid event types
};

// ---------------------------------------------------------------------------
// Inter-arrival time buckets
//   0 : < 100 ms
//   1 : 100 ms – 500 ms
//   2 : 500 ms – 2 s
//   3 : 2 s – 10 s
//   4 : > 10 s
// ---------------------------------------------------------------------------
static const int MK_IAT_BUCKETS = 5;

// Total state-space dimension: event_type x iat_bucket
static const int MK_N_STATES = MK_EVENT_COUNT * MK_IAT_BUCKETS; // 35

// ---------------------------------------------------------------------------
// Binary model file layout  (loaded from SPIFFS /models/markov_baseline.bin)
//
//   Offset  Len   Field
//   0       4     magic "MKV1" (0x4D 0x4B 0x56 0x31)
//   4       2     version (uint16_t LE, currently 1)
//   6       2     n_states (uint16_t LE, must equal MK_N_STATES)
//   8       2*n_states^2  transition matrix, row-major, int16_t LE, Q1.15
//                         entry = round(-log(p) * 32767)
//                         higher value → less likely → more anomalous
// ---------------------------------------------------------------------------
static const uint32_t MK_MAGIC = 0x31564B4Du; // little-endian "MKV1" (M=0x4D K=0x4B V=0x56 1=0x31)

// ---------------------------------------------------------------------------
// Scorer tuning constants
// ---------------------------------------------------------------------------
static const int  MK_WINDOW_LEN  = 32;  // rolling NLL window depth (per track)
static const int  MK_MAX_TRACKS  = 4;   // max simultaneous RID tracks in SRAM
static const int  MK_UNKNOWN_NLL_Q15 = 32767; // NLL for OOV state (max int16)

// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------

/**
 * markov_init — load baseline model from SPIFFS (Arduino) or stdio (native).
 *
 * @param baseline_path  SPIFFS path, e.g. "/models/markov_baseline.bin"
 * @return true on success; false if file missing, magic mismatch, or n_states
 *         does not equal MK_N_STATES.
 *
 * Must be called before any other markov_* function.
 * Safe to call multiple times (re-loads the model).
 */
bool markov_init(const char* baseline_path);

/**
 * markov_observe_event — feed one RID event for the active track.
 *
 * Computes the state index from (event_type, iat_bucket), looks up the
 * NLL from the transition matrix, and pushes it into the rolling window.
 *
 * Call markov_reset_track() before the first event for a new track_id to
 * ensure clean per-track state.  The scorer defaults to track slot 0 for
 * calls that do not use a track ID (single-track mode).
 *
 * @param event_type   One of MarkovEventType (0..MK_EVENT_COUNT-1).
 *                     Values >= MK_EVENT_COUNT are treated as OOV.
 * @param timestamp_ms Monotonic clock in milliseconds (e.g. millis() on
 *                     Arduino, or a synthetic counter under UNIT_TEST).
 */
void markov_observe_event(uint8_t event_type, uint32_t timestamp_ms);

/**
 * markov_observe_event_track — same as markov_observe_event but for a
 * specific track slot (0 .. MK_MAX_TRACKS-1).  If track_slot is out of
 * range the call is silently ignored.
 */
void markov_observe_event_track(uint8_t track_slot,
                                uint8_t event_type,
                                uint32_t timestamp_ms);

/**
 * markov_current_nll — rolling average NLL over the last MK_WINDOW_LEN
 * events for track slot 0.
 *
 * Returns 0.0f if no events have been observed yet.
 */
float markov_current_nll(void);

/**
 * markov_current_nll_track — per-track variant.
 */
float markov_current_nll_track(uint8_t track_slot);

/**
 * markov_is_anomalous — convenience wrapper.
 *
 * @param threshold  Caller-supplied NLL threshold.  Recommended start: 4.0f.
 * @return true if rolling NLL for track 0 exceeds threshold.
 */
bool markov_is_anomalous(float threshold);

/**
 * markov_reset_track — clear rolling window and last-state for a track slot.
 *
 * Call this when a new RID target is assigned to the slot, or when a track
 * times out and needs to be reused.
 *
 * @param track_id  Logical track identifier; selects slot via (track_id %
 *                  MK_MAX_TRACKS).  Pass 0 to reset slot 0.
 */
void markov_reset_track(uint32_t track_id);

/**
 * markov_publish_nll — emit one 16-byte MSG_TYPE_BEHAVIORAL_NLL packet
 * over the LoRa mesh so the gateway / OpenForge C2 can see this sensor's
 * behavioral anomaly score for `track_slot`.
 */
bool markov_publish_nll(uint32_t sensor_id, uint8_t track_slot, float threshold);

/** Runtime-tunable anomaly threshold (default 4.0). */
void  markov_set_anomaly_threshold(float threshold);
float markov_get_anomaly_threshold(void);

#endif // MARKOV_SCORER_H
