// ============================================================================
// ota_firmware.cpp — see ota_firmware.h for design notes and scope limits.
// ============================================================================

#include "ota_firmware.h"
#include "config.h"

#ifdef ARDUINO

#include <Arduino.h>
#include <WiFi.h>
#include <ArduinoOTA.h>
#include <esp_ota_ops.h>
#include <esp_task_wdt.h>
#include <nvs.h>
#include <cstring>

static const char* TAG = "OtaFirmware";

#define OTA_NVS_NS   "pdl_ota"
#define OTA_NVS_KEY  "otapass"

static bool s_started = false;
static char s_password[65] = {0};

static void load_password_from_nvs() {
    nvs_handle_t h;
    if (nvs_open(OTA_NVS_NS, NVS_READONLY, &h) != ESP_OK) return;
    size_t len = sizeof(s_password);
    if (nvs_get_str(h, OTA_NVS_KEY, s_password, &len) != ESP_OK) {
        s_password[0] = '\0';
    }
    nvs_close(h);
}

void ota_firmware_set_password(const char* password) {
    if (password == nullptr) password = "";
    strncpy(s_password, password, sizeof(s_password) - 1);
    s_password[sizeof(s_password) - 1] = '\0';

    nvs_handle_t h;
    if (nvs_open(OTA_NVS_NS, NVS_READWRITE, &h) == ESP_OK) {
        nvs_set_str(h, OTA_NVS_KEY, s_password);
        nvs_commit(h);
        nvs_close(h);
    }

    // Never echo the password itself — only whether one is now set.
    Serial.printf("[%s] OTA password %s. Restart to apply.\n",
                  TAG, s_password[0] ? "set" : "cleared");
}

static void start_ota() {
    ArduinoOTA.setHostname(SENSOR_NAME);
    if (s_password[0]) {
        ArduinoOTA.setPassword(s_password);
    }

    ArduinoOTA.onStart([]() {
        // CRITICAL: unsubscribe loopTask from the task watchdog for the
        // duration of the transfer.
        //
        // ArduinoOTA.handle() does NOT return until the whole image has been
        // received — it blocks inside its own recv loop. loopTask therefore
        // never gets back around to reset the WDT, and the watchdog aborts the
        // chip mid-download. Observed live on Spencer03 at ~60%:
        //     task_wdt: Task watchdog got triggered ... - loopTask (CPU 1)
        //     abort() ... rst:0xc (RTC_SW_CPU_RST)
        // Re-subscribed in onError; onEnd reboots so it does not matter there.
        esp_task_wdt_delete(NULL);

        // The SX1262 is deliberately left running. RadioLib holds no flash
        // resources, and tearing it down here would strand a half-configured
        // radio if the update aborts — which, as the WDT reset proved, is a
        // path that really does get taken.
        Serial.printf("[%s] Update starting (%s) — watchdog suspended.\n", TAG,
                      ArduinoOTA.getCommand() == U_FLASH ? "firmware" : "filesystem");
    });

    ArduinoOTA.onProgress([](unsigned int done, unsigned int total) {
        // Belt-and-braces: even unsubscribed, feed the WDT on every chunk so
        // that if a future core version re-subscribes loopTask internally, or
        // another task shares the timer, we still do not trip it.
        esp_task_wdt_reset();

        static unsigned last_pct = 999;
        unsigned pct = total ? (done * 100u / total) : 0u;
        if (pct != last_pct && pct % 10 == 0) {
            last_pct = pct;
            Serial.printf("[%s] %u%%\n", TAG, pct);
        }
    });

    ArduinoOTA.onEnd([]() {
        Serial.printf("[%s] Update complete — rebooting into the new image.\n", TAG);
    });

    ArduinoOTA.onError([](ota_error_t err) {
        const char* why = "unknown";
        switch (err) {
            case OTA_AUTH_ERROR:    why = "auth failed";    break;
            case OTA_BEGIN_ERROR:   why = "begin failed (no free OTA slot? check partition table)"; break;
            case OTA_CONNECT_ERROR: why = "connect failed"; break;
            case OTA_RECEIVE_ERROR: why = "receive failed"; break;
            case OTA_END_ERROR:     why = "end failed";     break;
        }
        // Put loopTask back under the watchdog — the unit carries on sensing
        // on the old image and must stay protected against a genuine hang.
        esp_task_wdt_add(NULL);

        // An aborted OTA leaves the RUNNING image untouched — that is the
        // whole point of A/B slots. The unit keeps sensing on the old firmware.
        Serial.printf("[%s] Update FAILED: %s. Still running the previous image.\n",
                      TAG, why);
    });

    ArduinoOTA.begin();
    s_started = true;

    Serial.printf("[%s] OTA listening as \"%s\" at %s%s\n", TAG, SENSOR_NAME,
                  WiFi.localIP().toString().c_str(),
                  s_password[0] ? " (password set)" : " (NO password — LAN trust)");
}

void ota_firmware_tick() {
    if (WiFi.status() != WL_CONNECTED) {
        // Don't tear down on a transient drop; ArduinoOTA re-binds on the next
        // successful association because s_started stays false until begun.
        return;
    }
    if (!s_started) {
        load_password_from_nvs();
        start_ota();
        return;
    }
    ArduinoOTA.handle();
}

bool ota_firmware_is_active() { return s_started; }

const char* ota_firmware_running_partition() {
    const esp_partition_t* p = esp_ota_get_running_partition();
    return (p && p->label[0]) ? p->label : "unknown";
}

#else  // !ARDUINO — native/test stub

void ota_firmware_tick() {}
void ota_firmware_set_password(const char*) {}
bool ota_firmware_is_active() { return false; }
const char* ota_firmware_running_partition() { return "native"; }

#endif
