#include "OtaApply.h"
#include "OtaFormat.h"
#include "MotaContainer.h"
#include "Identity.h"
#include <string.h>

#if defined(ESP32_PLATFORM)
  #include <SHA256.h>            // rweather streaming SHA-256 (for hashing the slot in chunks)
  #include "esp_ota_ops.h"
  #include "esp_partition.h"
  #include "esp_system.h"
  extern "C" {
    #include "detools/detools.h" // vendored detools 0.53.0 embeddable decoder (CRLE-only build)
  }
  #if defined(OTA_FLASH_STORE)
    #include "OtaStoreFlashEsp32.h"   // flash-staged container (delta patch / full image in the slot)
    #include "OtaSelf.h"              // SelfFwInfo / ota_self_firmware (running-image base_hash gate)
  #endif
#elif defined(NRF52_PLATFORM)
  #include "OtaVerify.h"
  #include "OtaSelf.h"
  #include "OtaFlashLayout_nrf52.h"
  #include "OtaBlInfo.h"            // read the bootloader capability marker before arming an apply
  #include "flash/flash_nrf5x.h"  // Adafruit core internal-flash driver (has its own extern "C")
  #include "nrf.h"
  #include "nrf_soc.h"
  #include "nrf_sdm.h"
#endif

namespace mesh {
namespace ota {

#if defined(ESP32_PLATFORM)

bool ota_apply_slot_info(uint32_t* addr, uint32_t* size) {
  const esp_partition_t* p = esp_ota_get_next_update_partition(nullptr);
  if (!p) return false;
  if (addr) *addr = p->address;
  if (size) *size = p->size;
  return true;
}

bool ota_apply_set_manifest(const uint8_t* mf, uint32_t len, const SignerAllowlist& allow, ApplyState& st) {
  st = ApplyState();
  ota_apply_slot_info(&st.slot_addr, &st.slot_size);
  MotaManifest m;
  if (!mota_parse_manifest(mf, len, m)) return false;
  if (!m.is_full()) return false;            // A/B apply takes a full image (delta would need decode)
  st.image_size = m.image_size;
  memcpy(st.image_hash, m.image_hash, 32);
  st.manifest_ok = true;
  if (m.is_signed()) {
    mesh::Identity signer(m.signer_pubkey);
    st.sig_ok = signer.verify(m.signature, m.manifest_start, (int)m.signed_len);
    st.trusted = st.sig_ok && allow.contains(m.signer_pubkey);
  }
  return true;
}

bool ota_apply_verify_slot(ApplyState& st) {
  st.slot_ok = false;
  if (!st.manifest_ok || st.image_size == 0 || st.image_size > st.slot_size) return false;
  const esp_partition_t* p = esp_ota_get_next_update_partition(nullptr);
  if (!p) return false;
  SHA256 sha;
  uint8_t buf[512];
  uint32_t off = 0;
  while (off < st.image_size) {
    uint32_t n = st.image_size - off; if (n > sizeof(buf)) n = sizeof(buf);
    if (esp_partition_read(p, off, buf, n) != ESP_OK) return false;
    sha.update(buf, n);
    off += n;
  }
  uint8_t h[32];
  sha.finalize(h, 32);
  st.slot_ok = (memcmp(h, st.image_hash, 32) == 0);
  return st.slot_ok;
}

bool ota_apply_commit() {
  const esp_partition_t* p = esp_ota_get_next_update_partition(nullptr);
  if (!p) return false;
  if (esp_ota_set_boot_partition(p) != ESP_OK) return false;
  esp_restart();   // does not return
  return true;
}

// --- detools callback context -----------------------------------------------------------------
// The delta base is the running OTA slot; the reconstructed image is streamed into the inactive slot
// via esp_ota_write (sequential, append-only -- matches detools' sequential output ordering) and
// hashed on the fly so we can check it against the signed manifest image_hash before arming.
struct DetoolsCtx {
  const esp_partition_t* base;    // delta base (running image), read at absolute `from_pos`
  long           from_pos;        // absolute byte offset into `base`
#if defined(OTA_FLASH_STORE)
  OtaStoreFlashEsp32* store;      // staged container; patch = payload region [patch_base, +patch_len)
  uint32_t       patch_base;      // container offset where the payload (patch) begins
#else
  const uint8_t* patch;           // .mota payload held wholly in RAM (RAM store; bring-up/host)
#endif
  uint32_t       patch_len;
  uint32_t       patch_pos;
  esp_ota_handle_t out;           // inactive slot write handle
  SHA256*        sha;             // running hash of the reconstructed output
  uint32_t       out_pos;         // #bytes written to the output slot
  bool           io_ok;
};

static int dt_from_read(void* arg, uint8_t* buf, size_t size) {
  DetoolsCtx* c = (DetoolsCtx*)arg;
  if (c->from_pos < 0 || (uint32_t)(c->from_pos) + size > c->base->size) return -DETOOLS_IO_FAILED;
  if (esp_partition_read(c->base, (size_t)c->from_pos, buf, size) != ESP_OK) { c->io_ok = false; return -DETOOLS_IO_FAILED; }
  c->from_pos += (long)size;
  return DETOOLS_OK;
}
static int dt_from_seek(void* arg, int offset) {          // detools uses relative seeks
  DetoolsCtx* c = (DetoolsCtx*)arg;
  c->from_pos += offset;
  if (c->from_pos < 0 || (uint32_t)c->from_pos > c->base->size) return -DETOOLS_IO_FAILED;
  return DETOOLS_OK;
}
static int dt_patch_read(void* arg, uint8_t* buf, size_t size) {
  DetoolsCtx* c = (DetoolsCtx*)arg;
  if (c->patch_pos + size > c->patch_len) return -DETOOLS_IO_FAILED;
#if defined(OTA_FLASH_STORE)
  if (!c->store->read(c->patch_base + c->patch_pos, buf, size)) { c->io_ok = false; return -DETOOLS_IO_FAILED; }
#else
  memcpy(buf, c->patch + c->patch_pos, size);
#endif
  c->patch_pos += (uint32_t)size;
  return DETOOLS_OK;
}
static int dt_to_write(void* arg, const uint8_t* buf, size_t size) {
  DetoolsCtx* c = (DetoolsCtx*)arg;
  if (esp_ota_write(c->out, buf, size) != ESP_OK) { c->io_ok = false; return -DETOOLS_IO_FAILED; }
  c->sha->update(buf, size);
  c->out_pos += (uint32_t)size;
  return DETOOLS_OK;
}

#if defined(OTA_FLASH_STORE)
// --- in-place delta on ESP32 (codec 2) ----------------------------------------------------------
// A single in-place `.mota` can target BOTH nRF52 (bootloader applies it) and ESP32. On ESP32 the
// inactive slot is used as the in-place working memory: we copy the running image (the base) into the
// slot's bottom-staged-container-FREE region [0, write_start), then run detools' in-place decoder over
// that region (it reads the base, erases segments, writes the target back), reading the patch from the
// staged container's payload (which lives at/below write_start, disjoint from the working region). The
// decoded image is hashed against the signed image_hash BEFORE arming, so a bad decode never boots; the
// callbacks are bounded to [0, write_start) so they fail gracefully instead of touching the patch.
// (Sequential is still preferred on ESP32 — it streams straight to the slot with no base-copy; in-place
// exists only for single-artifact distribution. Requires the patch built with --inplace-segment 4096.)
struct InPlaceCtx {
  const esp_partition_t* slot;    // in-place working memory = slot[0, mem_max)
  uint32_t       mem_max;         // = container write_start; accesses beyond this are refused
  OtaStoreFlashEsp32* store;      // staged container; patch = payload region [patch_base, +patch_len)
  uint32_t       patch_base, patch_len, patch_pos;
  int            step;            // detools resume cursor (RAM; no cross-reboot resume of the apply)
  bool           io_ok;
  const char*    fail;            // first failure point (diagnostic), nullptr until set
  uint32_t       fa, fn; int frc;  // failing addr / len / esp_err
};
static inline int ip_fail(InPlaceCtx* c, const char* w, uint32_t a, size_t n, int rc) {
  if (!c->fail) { c->fail = w; c->fa = a; c->fn = (uint32_t)n; c->frc = rc; }
  c->io_ok = false; return -DETOOLS_IO_FAILED;
}
static int ip_mem_read(void* a, void* dst, uintptr_t src, size_t n) {
  InPlaceCtx* c = (InPlaceCtx*)a;
  if ((uint32_t)src + n > c->mem_max) return ip_fail(c, "rd>max", (uint32_t)src, n, 0);
  int rc = esp_partition_read(c->slot, (size_t)src, dst, n);
  if (rc != ESP_OK) return ip_fail(c, "rd", (uint32_t)src, n, rc);
  return DETOOLS_OK;
}
static int ip_mem_write(void* a, uintptr_t dst, void* src, size_t n) {
  InPlaceCtx* c = (InPlaceCtx*)a;
  if ((uint32_t)dst + n > c->mem_max) return ip_fail(c, "wr>max", (uint32_t)dst, n, 0);
  int rc = esp_partition_write(c->slot, (size_t)dst, src, n);
  if (rc != ESP_OK) return ip_fail(c, "wr", (uint32_t)dst, n, rc);
  return DETOOLS_OK;
}
static int ip_mem_erase(void* a, uintptr_t addr, size_t n) {
  InPlaceCtx* c = (InPlaceCtx*)a;
  // esp_partition_erase_range requires a SECTOR-aligned size; detools' final in-place segment is partial
  // (the image tail past the last full sector). addr is sector-aligned (== --inplace-segment), so round
  // the length UP to a full sector. The over-erased bytes are scratch beyond image_size (never hashed),
  // and — since detools processes high→low and erases-before-writing — they are never live patch data.
  const uint32_t SEC = 4096;
  if ((uint32_t)addr % SEC != 0) return ip_fail(c, "er!align", (uint32_t)addr, n, 0);
  uint32_t len = ((uint32_t)n + SEC - 1) & ~(SEC - 1);
  if ((uint32_t)addr + len > c->mem_max) return ip_fail(c, "er>max", (uint32_t)addr, len, 0);
  int rc = esp_partition_erase_range(c->slot, (size_t)addr, len);
  if (rc != ESP_OK) return ip_fail(c, "er", (uint32_t)addr, len, rc);
  return DETOOLS_OK;
}
static int ip_step_set(void* a, int s) { ((InPlaceCtx*)a)->step = s; return DETOOLS_OK; }
static int ip_step_get(void* a, int* s) { *s = ((InPlaceCtx*)a)->step; return DETOOLS_OK; }
static int ip_patch_read(void* a, uint8_t* b, size_t n) {
  InPlaceCtx* c = (InPlaceCtx*)a;
  if (c->patch_pos + n > c->patch_len) return ip_fail(c, "patch>len", c->patch_pos, n, 0);
  if (!c->store->read(c->patch_base + c->patch_pos, b, n)) return ip_fail(c, "patch_rd", c->patch_pos, n, 0);
  c->patch_pos += (uint32_t)n;
  return DETOOLS_OK;
}

static bool esp32_inplace_apply(OtaStoreFlashEsp32& store, const MotaManifest& m, ApplyState& st, char* msg) {
  const esp_partition_t* slot = store.partition();
  const esp_partition_t* base = esp_ota_get_running_partition();
  if (!slot || !base) { strcpy(msg, "no slot/base partition"); return false; }
  SelfFwInfo fi;
  if (!ota_self_firmware(fi) || !fi.valid) { strcpy(msg, "cannot read running firmware (no EndF)"); return false; }
  if (!m.base_hash || memcmp(m.base_hash, fi.body_hash, 8) != 0) { strcpy(msg, "not built for the running firmware (base mismatch)"); return false; }
  uint32_t mem_max = store.write_start();          // working region [0, mem_max); the patch sits at/above it
  if (mem_max == 0) { strcpy(msg, "in-place needs a bottom-staged container"); return false; }
  if (fi.image_len > mem_max || m.image_size > mem_max) { strcpy(msg, "in-place region too small for base/image"); return false; }

  // load the base (running image) into the working region [0, base_len), sector by sector (erase + copy)
  uint8_t buf[512];
  for (uint32_t off = 0; off < fi.image_len; ) {
    uint32_t sec = off & ~(4096u - 1);
    if (esp_partition_erase_range(slot, sec, 4096) != ESP_OK) { strcpy(msg, "base erase failed"); return false; }
    uint32_t secend = sec + 4096; if (secend > fi.image_len) secend = fi.image_len;
    for (uint32_t p = (off > sec ? off : sec); p < secend; ) {
      uint32_t n = secend - p; if (n > sizeof(buf)) n = sizeof(buf);
      if (esp_partition_read(base, p, buf, n) != ESP_OK || esp_partition_write(slot, p, buf, n) != ESP_OK) {
        strcpy(msg, "base copy failed"); return false; }
      p += n;
    }
    off = secend;
  }

  // patch in place over the working region; patch streamed from the staged container payload
  InPlaceCtx c;
  c.slot = slot; c.mem_max = mem_max; c.store = &store;
  c.patch_base = store.meta_bytes(); c.patch_len = m.payload_size; c.patch_pos = 0; c.step = 0; c.io_ok = true;
  c.fail = nullptr; c.fa = c.fn = 0; c.frc = 0;
  int r = detools_apply_patch_in_place_callbacks(ip_mem_read, ip_mem_write, ip_mem_erase,
                                                 ip_step_set, ip_step_get, ip_patch_read,
                                                 (size_t)m.payload_size, &c);
  if (r < 0 || !c.io_ok) {
    if (c.fail) sprintf(msg, "in-place decode err %d @%s a=%u n=%u rc=%d max=%u", r, c.fail,
                        (unsigned)c.fa, (unsigned)c.fn, c.frc, (unsigned)mem_max);
    else sprintf(msg, "in-place decode err %d", r);
    return false;
  }
  if ((uint32_t)r != m.image_size) { sprintf(msg, "in-place size %u!=%u", (unsigned)r, (unsigned)m.image_size); return false; }

  // verify the decoded slot image against the signed image_hash BEFORE arming (mismatch -> never boots)
  SHA256 sha;
  for (uint32_t off = 0; off < m.image_size; ) {
    uint32_t n = m.image_size - off; if (n > sizeof(buf)) n = sizeof(buf);
    if (esp_partition_read(slot, off, buf, n) != ESP_OK) { strcpy(msg, "slot read failed"); return false; }
    sha.update(buf, n); off += n;
  }
  uint8_t hh[32]; sha.finalize(hh, 32);
  st.slot_ok = (memcmp(hh, m.image_hash, 32) == 0);
  if (!st.slot_ok) { strcpy(msg, "image_hash MISMATCH after in-place decode"); return false; }
  if (esp_ota_set_boot_partition(slot) != ESP_OK) { strcpy(msg, "set_boot failed"); return false; }
  sprintf(msg, "verified%s; in-place decoded %u B, image hash OK — armed, rebooting to apply",
          m.is_signed() ? " (signer trusted)" : " (unsigned)", (unsigned)m.image_size);
  return true;
}

// Apply the `.mota` staged in the inactive slot by OtaStoreFlashEsp32 (no contiguous RAM copy).
//   FULL: the payload was streamed straight to slot offset 0 during the fetch -> hash the slot image
//         and compare to the signed image_hash, then arm. No decode, no copy.
//   DELTA (sequential): base = the running slot; the patch is read from the staged payload region (the
//         slot's bottom); the reconstructed image is written to the inactive slot via esp_ota_write and
//         hashed vs image_hash. esp_ota_begin only erases [0, image_size], which the fetch-time fit
//         check kept below the bottom-staged container, so the patch survives while we decode over it.
//   DELTA (in-place): copy the running image into the slot's working region then patch in place
//         (esp32_inplace_apply); image_hash-gated before arming. Lets one in-place .mota target both
//         ESP32 and nRF52. Sequential is still preferred on ESP32 (no base-copy).
// The result is verified (signature/trust up front, image_hash after) and the slot armed; the caller
// reboots once the confirmation reply has gone out.
bool ota_apply_detools_mota(OtaStoreFlashEsp32& store, const SignerAllowlist& allow, ApplyState& st, char* msg) {
  st = ApplyState();
  const esp_partition_t* slot = store.partition();
  if (!slot || store.staged_size() < 16) { strcpy(msg, "no staged update"); return false; }
  st.slot_addr = slot->address; st.slot_size = slot->size;

  // read + parse the manifest out of the staged container (header = MAGIC(4) + total(4))
  uint8_t hdr[8];
  if (!store.read(0, hdr, 8) || memcmp(hdr, MOTA_MAGIC, 4) != 0) { strcpy(msg, "bad container"); return false; }
  uint8_t mfbuf[256];
  uint32_t mflen = store.meta_bytes() > 8 ? store.meta_bytes() - 8 : 0;   // manifest+leaves; cap to mfbuf
  if (mflen > sizeof(mfbuf)) mflen = sizeof(mfbuf);
  MotaManifest m;
  if (mflen < 57 || !store.read(8, mfbuf, mflen) || !mota_parse_manifest(mfbuf, mflen, m)) {
    strcpy(msg, "manifest parse failed"); return false; }
  st.image_size = m.image_size; memcpy(st.image_hash, m.image_hash, 32); st.manifest_ok = true;
  if (m.image_size == 0 || m.image_size > slot->size) { strcpy(msg, "image > slot"); return false; }

  // signature / trust BEFORE arming an untrusted image (image_hash below is the target-firmware gate)
  if (m.is_signed()) {
    mesh::Identity signer(m.signer_pubkey);
    st.sig_ok = signer.verify(m.signature, m.manifest_start, (int)m.signed_len);
    st.trusted = st.sig_ok && allow.contains(m.signer_pubkey);
    if (!st.sig_ok)  { strcpy(msg, "bad signature"); return false; }
    if (!st.trusted) { strcpy(msg, "untrusted signer (pubkey not in allowlist)"); return false; }
  }

  // ---- FULL: payload already in slot[0]; verify hash + arm ----
  if (m.is_full()) {
    SHA256 sha; uint8_t buf[512];
    for (uint32_t off = 0; off < m.image_size; ) {
      uint32_t n = m.image_size - off; if (n > sizeof(buf)) n = sizeof(buf);
      if (esp_partition_read(slot, off, buf, n) != ESP_OK) { strcpy(msg, "slot read failed"); return false; }
      sha.update(buf, n); off += n;
    }
    uint8_t hh[32]; sha.finalize(hh, 32);
    st.slot_ok = (memcmp(hh, m.image_hash, 32) == 0);
    if (!st.slot_ok) { strcpy(msg, "image_hash MISMATCH (slot)"); return false; }
    if (esp_ota_set_boot_partition(slot) != ESP_OK) { strcpy(msg, "set_boot failed"); return false; }
    sprintf(msg, "verified%s full image %u B in slot — armed, rebooting to apply",
            m.is_signed() ? " (trusted)" : " (unsigned)", (unsigned)m.image_size);
    return true;
  }

  // ---- DELTA ----
  if (m.codec_id == CODEC_DETOOLS_INPLACE) return esp32_inplace_apply(store, m, st, msg);  // single-artifact codec
  if (m.codec_id != CODEC_DETOOLS_SEQUENTIAL) { strcpy(msg, "unknown delta codec"); return false; }

  // delta must be built for the running firmware (cheap early gate; image_hash is the definitive check)
  if (m.base_hash) {
    SelfFwInfo fi;
    if (!ota_self_firmware(fi) || !fi.valid) { strcpy(msg, "cannot read running firmware (no EndF)"); return false; }
    if (memcmp(m.base_hash, fi.body_hash, 8) != 0) { strcpy(msg, "delta not built for the running firmware (base mismatch)"); return false; }
  }

  const esp_partition_t* base = esp_ota_get_running_partition();
  if (!base) { strcpy(msg, "no running partition"); return false; }
  esp_ota_handle_t h;
  if (esp_ota_begin(slot, m.image_size, &h) != ESP_OK) { strcpy(msg, "ota_begin failed"); return false; }

  SHA256 sha;
  DetoolsCtx ctx;
  ctx.base = base; ctx.from_pos = 0;
  ctx.store = &store; ctx.patch_base = store.meta_bytes(); ctx.patch_len = m.payload_size; ctx.patch_pos = 0;
  ctx.out = h; ctx.sha = &sha; ctx.out_pos = 0; ctx.io_ok = true;

  int r = detools_apply_patch_callbacks(dt_from_read, dt_from_seek, dt_patch_read,
                                        (size_t)m.payload_size, dt_to_write, &ctx);
  if (r < 0 || !ctx.io_ok) { esp_ota_abort(h); sprintf(msg, "detools err %d @%u/%u",
                             ctx.io_ok ? r : -DETOOLS_IO_FAILED, (unsigned)ctx.out_pos, (unsigned)m.image_size); return false; }
  if ((uint32_t)r != m.image_size || ctx.out_pos != m.image_size) {
    esp_ota_abort(h); sprintf(msg, "size mismatch %u!=%u", (unsigned)ctx.out_pos, (unsigned)m.image_size); return false; }
  uint8_t hh[32]; sha.finalize(hh, 32);
  st.slot_ok = (memcmp(hh, m.image_hash, 32) == 0);
  if (!st.slot_ok) { esp_ota_abort(h); strcpy(msg, "image_hash MISMATCH after decode"); return false; }
  if (esp_ota_end(h) != ESP_OK) { strcpy(msg, "ota_end failed"); return false; }
  if (esp_ota_set_boot_partition(slot) != ESP_OK) { strcpy(msg, "set_boot failed"); return false; }
  sprintf(msg, "verified%s; decoded %u B, image hash OK — armed, rebooting to apply",
          m.is_signed() ? " (signer trusted)" : " (unsigned)", (unsigned)m.image_size);
  return true;
}

#else  // !OTA_FLASH_STORE: RAM-staged apply (whole .mota in a contiguous RAM buffer; bring-up/host)

bool ota_apply_detools_mota(const uint8_t* buf, uint32_t len, const SignerAllowlist& allow,
                            ApplyState& st, char* msg) {
  st = ApplyState();
  MotaManifest m;
  if (!mota_parse(buf, len, m)) { strcpy(msg, "no valid .mota (parse failed)"); return false; }
  if (m.is_full() || m.codec_id != CODEC_DETOOLS_SEQUENTIAL) { strcpy(msg, "not a detools-sequential delta"); return false; }
  st.image_size = m.image_size;
  memcpy(st.image_hash, m.image_hash, 32);
  st.manifest_ok = true;
  if (m.is_signed()) {
    mesh::Identity signer(m.signer_pubkey);
    st.sig_ok = signer.verify(m.signature, m.manifest_start, (int)m.signed_len);
    st.trusted = st.sig_ok && allow.contains(m.signer_pubkey);
    if (!st.sig_ok)  { strcpy(msg, "bad signature"); return false; }
    if (!st.trusted) { strcpy(msg, "untrusted signer (pubkey not in allowlist)"); return false; }
  }
  const esp_partition_t* base = esp_ota_get_running_partition();
  const esp_partition_t* out  = esp_ota_get_next_update_partition(nullptr);
  if (!base || !out) { strcpy(msg, "no A/B slot"); return false; }
  st.slot_addr = out->address; st.slot_size = out->size;
  if (m.image_size > out->size) { strcpy(msg, "image > slot"); return false; }
  esp_ota_handle_t h;
  if (esp_ota_begin(out, m.image_size, &h) != ESP_OK) { strcpy(msg, "ota_begin failed"); return false; }
  SHA256 sha;
  DetoolsCtx ctx;
  ctx.base = base; ctx.from_pos = 0;
  ctx.patch = m.payload; ctx.patch_len = m.payload_size; ctx.patch_pos = 0;
  ctx.out = h; ctx.sha = &sha; ctx.out_pos = 0; ctx.io_ok = true;
  int r = detools_apply_patch_callbacks(dt_from_read, dt_from_seek, dt_patch_read,
                                        (size_t)m.payload_size, dt_to_write, &ctx);
  if (r < 0 || !ctx.io_ok) { esp_ota_abort(h); sprintf(msg, "detools err %d @%u/%u",
                             ctx.io_ok ? r : -DETOOLS_IO_FAILED, (unsigned)ctx.out_pos, (unsigned)m.image_size); return false; }
  if ((uint32_t)r != m.image_size || ctx.out_pos != m.image_size) {
    esp_ota_abort(h); sprintf(msg, "size mismatch %u!=%u", (unsigned)ctx.out_pos, (unsigned)m.image_size); return false; }
  uint8_t hh[32]; sha.finalize(hh, 32);
  st.slot_ok = (memcmp(hh, m.image_hash, 32) == 0);
  if (!st.slot_ok) { esp_ota_abort(h); strcpy(msg, "image_hash MISMATCH after decode"); return false; }
  if (esp_ota_end(h) != ESP_OK) { strcpy(msg, "ota_end failed"); return false; }
  if (esp_ota_set_boot_partition(out) != ESP_OK) { strcpy(msg, "set_boot failed"); return false; }
  sprintf(msg, "verified%s; decoded %u B, image hash OK — armed, rebooting to apply",
          m.is_signed() ? " (signer trusted)" : " (unsigned)", (unsigned)m.image_size);
  return true;
}
#endif // OTA_FLASH_STORE

bool ota_apply_mota_nrf52(const uint8_t*, uint32_t, const SignerAllowlist&, ApplyState& st, char* msg) {
  st = ApplyState(); strcpy(msg, "nRF52-only (ESP32 uses ota_apply_detools_mota)"); return false;
}

void ota_reboot_to_apply() { esp_restart(); }  // boots the slot armed by ota_apply_detools_mota; no return

#elif defined(NRF52_PLATFORM)  // single-slot: verify + mark APPROVED + hand off to the bootloader

// ESP32 A/B-only entry points are unsupported on nRF52.
bool ota_apply_slot_info(uint32_t*, uint32_t*) { return false; }
bool ota_apply_set_manifest(const uint8_t*, uint32_t, const SignerAllowlist&, ApplyState& st) { st = ApplyState(); return false; }
bool ota_apply_verify_slot(ApplyState&) { return false; }
bool ota_apply_commit() { return false; }
bool ota_apply_detools_mota(const uint8_t*, uint32_t, const SignerAllowlist&, ApplyState& st, char* msg) { st = ApplyState(); strcpy(msg, "use ota_apply_mota_nrf52"); return false; }

void ota_reboot_to_apply() {                   // public: set the apply magic + reset (does not return)
  uint8_t sd_en = 0;
  sd_softdevice_is_enabled(&sd_en);
  if (sd_en) {                                 // POWER is SD-restricted while the SoftDevice runs
    sd_power_gpregret_clr(0, 0xFFFFFFFF);
    sd_power_gpregret_set(0, GPREGRET_OTA_APPLY);
  } else {
    NRF_POWER->GPREGRET = GPREGRET_OTA_APPLY;
  }
  NVIC_SystemReset();                          // does not return
}

uint8_t ota_bootloader_last_rc() {             // bootloader's last in-place-apply code, stashed in GPREGRET2
  uint32_t v = 0;
  uint8_t en = 0; sd_softdevice_is_enabled(&en);
  if (en) sd_power_gpregret_get(1, &v); else v = NRF_POWER->GPREGRET2;
  return (uint8_t)v;
}

bool ota_apply_mota_nrf52(const uint8_t* buf, uint32_t len, const SignerAllowlist& allow,
                          ApplyState& st, char* msg) {
  st = ApplyState();
  MotaManifest m;
  if (!mota_parse(buf, len, m)) { strcpy(msg, "parse failed"); return false; }
  if (m.is_full() || m.codec_id != CODEC_DETOOLS_INPLACE) { strcpy(msg, "not an in-place delta"); return false; }
  st.image_size = m.image_size;
  memcpy(st.image_hash, m.image_hash, 32);
  st.manifest_ok = true;

  // 0) THIS device's bootloader must be able to apply this .mota — otherwise staging + approving + rebooting
  //    just bounces back unchanged (a legacy/stock/older-OTAFIX bootloader). Refuse here, before any reboot.
  {
    OtaBlCaps bl = ota_bootloader_caps();
    if (!bl.present) { strcpy(msg, "this bootloader has no OTA-apply support — update the bootloader first"); return false; }
    if (bl.apply_abi < m.format_ver || !(bl.codec_mask & (1u << m.codec_id))) {
      snprintf(msg, 159, "bootloader too old to apply this update (bl abi=%u codecs=0x%x; need fmt>=%u codec=%u) — update the bootloader",
               bl.apply_abi, bl.codec_mask, m.format_ver, m.codec_id);
      return false;
    }
  }

  // Gated verification, in order, returning the FIRST failing reason (the bootloader re-checks integrity
  // again before booting, so authenticity is gated here and re-validated there):
  VerifyResult vr = ota_verify(buf, len, allow);
  st.sig_ok = vr.sig_ok; st.trusted = vr.trusted;

  // 1) downloaded payload: the fetched blocks must match the manifest's merkle root (intact + complete)
  if (!vr.root_ok || !vr.payload_ok || !vr.image_ok) {
    strcpy(msg, "payload hash mismatch (incomplete or corrupt .mota)"); return false;
  }

  // 2) target firmware: the delta must be built against THIS running image (base_hash == our EndF body
  //    hash). The resulting image_hash is re-checked by the bootloader after the in-place decode -- a
  //    single-slot device cannot produce the target image to hash it before applying.
  SelfFwInfo fi;
  if (!ota_self_firmware(fi) || !fi.valid) { strcpy(msg, "cannot read running firmware (no EndF)"); return false; }
  if (!m.base_hash || memcmp(m.base_hash, fi.body_hash, 8) != 0) { strcpy(msg, "not built for the running firmware (base mismatch)"); return false; }
  st.slot_ok = true;

  // 3) signature (only if the .mota is signed): valid Ed25519 AND signer in this device's allowlist
  if (vr.is_signed) {
    if (!vr.sig_ok)  { strcpy(msg, "bad signature"); return false; }
    if (!vr.trusted) { strcpy(msg, "untrusted signer (pubkey not in allowlist)"); return false; }
  }

  // mark the staged manifest APPROVED in flash (buf is the memory-mapped staging region, so
  // m.approval is a real flash address). NOR-clear over the erased 0xFFFFFFFF -> "APRV".
  uint32_t approval_addr = (uint32_t)(uintptr_t)m.approval;
  if (flash_nrf5x_write(approval_addr, APPROVAL_YES, 4) < 0) { strcpy(msg, "approval write failed"); return false; }
  flash_nrf5x_flush();
  if (memcmp((const void*)(uintptr_t)approval_addr, APPROVAL_YES, 4) != 0) { strcpy(msg, "approval not set"); return false; }

  // Approved. Do NOT reset here — return so the caller can deliver `msg` to the operator first; the
  // deferred ota_reboot_to_apply() (after the reply is sent) does the actual handoff to the bootloader.
  sprintf(msg, "verified%s; applying — rebooting into bootloader once this reply is sent",
          vr.is_signed ? " (signer trusted)" : " (unsigned)");
  return true;
}

#else  // native / other platforms

bool ota_apply_slot_info(uint32_t*, uint32_t*) { return false; }
bool ota_apply_set_manifest(const uint8_t*, uint32_t, const SignerAllowlist&, ApplyState& st) { st = ApplyState(); return false; }
bool ota_apply_verify_slot(ApplyState&) { return false; }
bool ota_apply_commit() { return false; }
bool ota_apply_detools_mota(const uint8_t*, uint32_t, const SignerAllowlist&, ApplyState& st, char* msg) { st = ApplyState(); strcpy(msg, "unsupported"); return false; }
bool ota_apply_mota_nrf52(const uint8_t*, uint32_t, const SignerAllowlist&, ApplyState& st, char* msg) { st = ApplyState(); strcpy(msg, "unsupported"); return false; }
void ota_reboot_to_apply() {}
uint8_t ota_bootloader_last_rc() { return 0; }

#endif

} // namespace ota
} // namespace mesh
