/* gitea_ota.c — Gitea version checker (bd-3hte)
 *
 * Uses esp_http_client + cJSON to query:
 *   GET /api/v1/repos/{repo}/releases?limit=10
 * Filters releases by tag prefix, extracts version and download URLs.
 */

#include "gitea_ota.h"
#include "version.h"
#include "esp_log.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_netif.h"
#include "esp_http_client.h"
#include "nvs_flash.h"
#include "nvs.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/event_groups.h"
#include "cJSON.h"
#include <string.h>
#include <stdio.h>

static const char *TAG = "gitea_ota";

ota_update_info_t g_balance_update = {0};
ota_update_info_t g_io_update      = {0};

/* ── WiFi connection ── */
#define WIFI_CONNECTED_BIT  BIT0
#define WIFI_FAIL_BIT       BIT1
#define WIFI_MAX_RETRIES    5

/* Compile-time WiFi fallback (override in NVS "wifi"/"ssid","pass") */
#define DEFAULT_WIFI_SSID   "saltylab"
#define DEFAULT_WIFI_PASS   ""

static EventGroupHandle_t s_wifi_eg;
static int s_wifi_retries = 0;

static void wifi_event_handler(void *arg, esp_event_base_t base,
                                int32_t id, void *data)
{
    if (base == WIFI_EVENT && id == WIFI_EVENT_STA_START) {
        esp_wifi_connect();
    } else if (base == WIFI_EVENT && id == WIFI_EVENT_STA_DISCONNECTED) {
        if (s_wifi_retries < WIFI_MAX_RETRIES) {
            esp_wifi_connect();
            s_wifi_retries++;
        } else {
            xEventGroupSetBits(s_wifi_eg, WIFI_FAIL_BIT);
        }
    } else if (base == IP_EVENT && id == IP_EVENT_STA_GOT_IP) {
        s_wifi_retries = 0;
        xEventGroupSetBits(s_wifi_eg, WIFI_CONNECTED_BIT);
    }
}

static bool wifi_connect(void)
{
    char ssid[64] = DEFAULT_WIFI_SSID;
    char pass[64] = DEFAULT_WIFI_PASS;

    /* Try to read credentials from NVS */
    nvs_handle_t nvs;
    if (nvs_open("wifi", NVS_READONLY, &nvs) == ESP_OK) {
        size_t sz = sizeof(ssid);
        nvs_get_str(nvs, "ssid", ssid, &sz);
        sz = sizeof(pass);
        nvs_get_str(nvs, "pass", pass, &sz);
        nvs_close(nvs);
    }

    s_wifi_eg = xEventGroupCreate();
    s_wifi_retries = 0;

    ESP_ERROR_CHECK(esp_netif_init());
    ESP_ERROR_CHECK(esp_event_loop_create_default());
    esp_netif_create_default_wifi_sta();

    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
    ESP_ERROR_CHECK(esp_wifi_init(&cfg));

    esp_event_handler_instance_t h1, h2;
    ESP_ERROR_CHECK(esp_event_handler_instance_register(
        WIFI_EVENT, ESP_EVENT_ANY_ID, wifi_event_handler, NULL, &h1));
    ESP_ERROR_CHECK(esp_event_handler_instance_register(
        IP_EVENT, IP_EVENT_STA_GOT_IP, wifi_event_handler, NULL, &h2));

    wifi_config_t wcfg = {0};
    strlcpy((char *)wcfg.sta.ssid,     ssid, sizeof(wcfg.sta.ssid));
    strlcpy((char *)wcfg.sta.password, pass, sizeof(wcfg.sta.password));

    ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
    ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wcfg));
    ESP_ERROR_CHECK(esp_wifi_start());

    EventBits_t bits = xEventGroupWaitBits(s_wifi_eg,
        WIFI_CONNECTED_BIT | WIFI_FAIL_BIT, pdFALSE, pdFALSE,
        pdMS_TO_TICKS(15000));

    esp_event_handler_instance_unregister(IP_EVENT, IP_EVENT_STA_GOT_IP, h2);
    esp_event_handler_instance_unregister(WIFI_EVENT, ESP_EVENT_ANY_ID, h1);
    vEventGroupDelete(s_wifi_eg);

    if (bits & WIFI_CONNECTED_BIT) {
        ESP_LOGI(TAG, "WiFi connected SSID=%s", ssid);
        return true;
    }
    ESP_LOGW(TAG, "WiFi connect failed SSID=%s", ssid);
    return false;
}

/* ── HTTP fetch into a heap buffer ── */
#define HTTP_RESP_MAX   (8 * 1024)

typedef struct { char *buf; int len; int cap; } http_buf_t;

static esp_err_t http_event_cb(esp_http_client_event_t *evt)
{
    http_buf_t *b = (http_buf_t *)evt->user_data;
    if (evt->event_id == HTTP_EVENT_ON_DATA && b) {
        if (b->len + evt->data_len < b->cap) {
            memcpy(b->buf + b->len, evt->data, evt->data_len);
            b->len += evt->data_len;
        }
    }
    return ESP_OK;
}

static char *http_get(const char *url)
{
    char *buf = malloc(HTTP_RESP_MAX);
    if (!buf) return NULL;
    http_buf_t b = {.buf = buf, .len = 0, .cap = HTTP_RESP_MAX};
    buf[0] = '\0';

    esp_http_client_config_t cfg = {
        .url            = url,
        .event_handler  = http_event_cb,
        .user_data      = &b,
        .timeout_ms     = GITEA_API_TIMEOUT_MS,
        .skip_cert_common_name_check = true,
    };
    esp_http_client_handle_t client = esp_http_client_init(&cfg);
    esp_err_t err = esp_http_client_perform(client);
    int status = esp_http_client_get_status_code(client);
    esp_http_client_cleanup(client);

    if (err != ESP_OK || status != 200) {
        ESP_LOGW(TAG, "HTTP GET %s → err=%d status=%d", url, err, status);
        free(buf);
        return NULL;
    }
    buf[b.len] = '\0';
    return buf;
}

/* ── Version comparison: returns true if remote > local ── */
static bool version_newer(const char *local, const char *remote)
{
    int la=0,lb=0,lc=0, ra=0,rb=0,rc=0;
    sscanf(local,  "%d.%d.%d", &la, &lb, &lc);
    sscanf(remote, "%d.%d.%d", &ra, &rb, &rc);
    if (ra != la) return ra > la;
    if (rb != lb) return rb > lb;
    return rc > lc;
}

/* ── Parse releases JSON array, fill ota_update_info_t ── */
static void parse_releases(const char *json, const char *tag_prefix,
                             const char *bin_asset, const char *sha_asset,
                             const char *local_version,
                             ota_update_info_t *out)
{
    cJSON *arr = cJSON_Parse(json);
    if (!arr || !cJSON_IsArray(arr)) {
        ESP_LOGW(TAG, "JSON parse failed");
        cJSON_Delete(arr);
        return;
    }

    cJSON *rel;
    cJSON_ArrayForEach(rel, arr) {
        cJSON *tag_j = cJSON_GetObjectItem(rel, "tag_name");
        if (!cJSON_IsString(tag_j)) continue;

        const char *tag = tag_j->valuestring;
        if (strncmp(tag, tag_prefix, strlen(tag_prefix)) != 0) continue;

        /* Extract version after prefix */
        const char *ver = tag + strlen(tag_prefix);
        if (*ver == 'v') ver++;  /* strip leading 'v' */

        if (!version_newer(local_version, ver)) continue;

        /* Found a newer release — extract asset URLs */
        cJSON *assets = cJSON_GetObjectItem(rel, "assets");
        if (!cJSON_IsArray(assets)) continue;

        out->available = false;
        out->download_url[0] = '\0';
        out->sha256[0] = '\0';
        strlcpy(out->version, ver, sizeof(out->version));

        cJSON *asset;
        cJSON_ArrayForEach(asset, assets) {
            cJSON *name_j = cJSON_GetObjectItem(asset, "name");
            cJSON *url_j  = cJSON_GetObjectItem(asset, "browser_download_url");
            if (!cJSON_IsString(name_j) || !cJSON_IsString(url_j)) continue;

            if (strcmp(name_j->valuestring, bin_asset) == 0) {
                strlcpy(out->download_url, url_j->valuestring,
                        sizeof(out->download_url));
                out->available = true;
            } else if (strcmp(name_j->valuestring, sha_asset) == 0) {
                /* Download the SHA256 asset inline */
                char *sha = http_get(url_j->valuestring);
                if (sha) {
                    /* sha file is just hex+newline */
                    size_t n = strspn(sha, "0123456789abcdefABCDEF");
                    if (n == 64) {
                        memcpy(out->sha256, sha, 64);
                        out->sha256[64] = '\0';
                    }
                    free(sha);
                }
            }
        }

        if (out->available) {
            ESP_LOGI(TAG, "update: tag=%s ver=%s", tag, out->version);
        }
        break;  /* use first matching release */
    }

    cJSON_Delete(arr);
}

/* ── Main check ── */
void gitea_ota_check_now(void)
{
    char url[512];
    snprintf(url, sizeof(url),
             "%s/api/v1/repos/%s/releases?limit=10",
             GITEA_BASE_URL, GITEA_REPO);

    char *json = http_get(url);
    if (!json) {
        ESP_LOGW(TAG, "releases fetch failed");
        return;
    }

    parse_releases(json, BALANCE_TAG_PREFIX, BALANCE_BIN_ASSET,
                   BALANCE_SHA256_ASSET, BALANCE_FW_VERSION, &g_balance_update);
    parse_releases(json, IO_TAG_PREFIX, IO_BIN_ASSET,
                   IO_SHA256_ASSET, IO_FW_VERSION, &g_io_update);
    free(json);
}

/* ── Background task ── */
static void version_check_task(void *arg)
{
    /* Initial check immediately after WiFi up */
    vTaskDelay(pdMS_TO_TICKS(2000));
    gitea_ota_check_now();

    for (;;) {
        vTaskDelay(pdMS_TO_TICKS(VERSION_CHECK_PERIOD_MS));
        gitea_ota_check_now();
    }
}

void gitea_ota_init(void)
{
    ESP_ERROR_CHECK(nvs_flash_init());

    if (!wifi_connect()) {
        ESP_LOGW(TAG, "WiFi unavailable — version checks disabled");
        return;
    }

    xTaskCreate(version_check_task, "ver_check", 6144, NULL, 3, NULL);
    ESP_LOGI(TAG, "version check task started");
}
