// scenario_mesh — ESP-NOW transport for Runtime 3 IR hot-load (Phase 2).
//
// Implements tasks 4 & 5 of docs/specs/2026-05-24-firmware-scenario-hotload.md:
//
//   * Frame protocol: the IR JSON blob is chunked into ESP-NOW frames of
//     <= SCENARIO_MESH_FRAME_MAX (240) bytes. Each frame carries a 4-byte
//     header { seq:u16, total:u16 } (little-endian on the wire) followed by
//     up to SCENARIO_MESH_PAYLOAD_MAX payload bytes.
//   * Sender (master): scenario_mesh_send() resolves an alias to a MAC,
//     registers the peer, and transmits every frame sequentially, awaiting
//     the per-frame esp_now send-callback ack before advancing.
//   * Receiver (peer board): the registered esp_now recv callback accumulates
//     frames keyed by (sender MAC + total) until `total` frames have arrived,
//     concatenates them, and hands the reassembled buffer to the apply
//     callback supplied at init — the same internal `_scenario_apply()` path
//     the HTTP POST /game/scenario handler uses.
//
// NOTE ON THE "EXISTING PEER REGISTRY":
//   The spec references an existing ESP-NOW peer registry + `espnow_recv_cb`
//   to extend. In the IDF tree (idf_zacus) no such registry exists yet — the
//   only ESP-NOW code is the legacy Arduino lib/espnow_common (broadcast-only,
//   puzzle-id keyed, not an IDF component). So this component carries its own
//   minimal alias->MAC table (scenario_mesh_register_peer / _mac_for_alias).
//   When a real shared registry lands, point mac_for_alias() at it.

#pragma once

#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>

#include "esp_err.h"

#ifdef __cplusplus
extern "C" {
#endif

// ESP-NOW hard limit on a single payload is 250 bytes. We cap the whole
// frame (header + payload) at 240 to stay clear of vendor headers and keep a
// safety margin, matching the spec's "<= 240-byte ESP-NOW frames".
#define SCENARIO_MESH_FRAME_MAX     240
#define SCENARIO_MESH_HEADER_BYTES  4
#define SCENARIO_MESH_PAYLOAD_MAX   (SCENARIO_MESH_FRAME_MAX - SCENARIO_MESH_HEADER_BYTES)  // 236

// Largest IR blob we will reassemble on the receive side. Mirrors
// GAME_ENDPOINT_MAX_SCENARIO_BYTES (64 KiB). 64 KiB / 236 ≈ 285 frames, well
// under the u16 sequence space.
#define SCENARIO_MESH_MAX_BLOB      (64 * 1024)

// Per-frame ack timeout. ESP-NOW send-callbacks normally fire within a few ms;
// a generous window absorbs RF retries without stalling the relay loop.
#define SCENARIO_MESH_ACK_TIMEOUT_MS  300

// Callback invoked on the receive side once a full blob has been reassembled.
// `data` is a NUL-terminated buffer of `len` bytes (the IR JSON). The callback
// must NOT take ownership — the buffer is freed by scenario_mesh after return.
// Return ESP_OK if the scenario was applied; any other value is logged.
typedef esp_err_t (*scenario_mesh_apply_cb_t)(const char *data, size_t len);

// Initialize ESP-NOW (idempotent — tolerates an already-initialized stack),
// register the send + recv callbacks, and register the broadcast peer.
//
// `apply_cb` may be NULL on a pure sender (master) that never receives
// scenarios; pass the board's _scenario_apply wrapper on receiver boards.
esp_err_t scenario_mesh_init(scenario_mesh_apply_cb_t apply_cb);

// Register / update an alias -> MAC mapping in the local peer table and add the
// MAC as an unencrypted ESP-NOW peer. Safe to call repeatedly with the same
// alias (updates the MAC). Returns ESP_ERR_NO_MEM if the table is full.
esp_err_t scenario_mesh_register_peer(const char *alias, const uint8_t mac[6]);

// Resolve an alias to its MAC. Returns ESP_OK and fills `mac_out` on hit,
// ESP_ERR_NOT_FOUND otherwise.
esp_err_t scenario_mesh_mac_for_alias(const char *alias, uint8_t mac_out[6]);

// Number of peers currently registered in the live table (broadcast peer
// excluded). Backs the espnow_peers health counter.
int scenario_mesh_peer_count(void);

// Chunk `data` (len bytes) into frames and send them all sequentially to
// `dest_mac`, awaiting the per-frame ack. Returns ESP_OK only if every frame
// was acked; ESP_ERR_TIMEOUT if any frame ack timed out, or the underlying
// esp_now_send error. The caller (relay handler) treats a non-OK return as a
// skipped peer and continues with the others.
esp_err_t scenario_mesh_send(const uint8_t dest_mac[6],
                             const char *data, size_t len);

// ─── short text frames: commands & events (spec 2026-06-11) ────────────────
//
// Single-frame messages riding the same ESP-NOW link as IR transfers. Wire
// compat: header seq=0xFFFF (sentinel) + total=kind — old receivers drop
// them via their `seq >= total` malformed check. Payload: UTF-8 text,
// NUL excluded, <= SCENARIO_MESH_TEXT_MAX bytes.

#define SCENARIO_MESH_TEXT_CMD  1   // master -> annex: do something
#define SCENARIO_MESH_TEXT_EVT  2   // annex -> master: something happened
#define SCENARIO_MESH_TEXT_MAX  200

// kind, sender MAC, NUL-terminated text. Runs on a dedicated worker task
// (never the Wi-Fi callback); keep it reasonably short anyway.
typedef void (*scenario_mesh_text_cb_t)(uint8_t kind,
                                        const uint8_t src_mac[6],
                                        const char *text);

// Send one CMD/EVT frame (kind = SCENARIO_MESH_TEXT_*). Unicast awaits the
// radio ack (ESP_ERR_TIMEOUT if the peer is silent); broadcast returns once
// transmitted. dest_mac may be the broadcast address FF:FF:FF:FF:FF:FF.
esp_err_t scenario_mesh_send_text(const uint8_t dest_mac[6], uint8_t kind,
                                  const char *text);

// Install (or replace, NULL to clear) the receive handler for CMD/EVT
// frames. Spawns the dispatch worker on first use.
esp_err_t scenario_mesh_set_text_cb(scenario_mesh_text_cb_t cb);

#ifdef __cplusplus
}
#endif
