/* =============================================================================
 * app_sht30.c - SHT30 온습도 보드 애플리케이션 태스크
 *
 * RPi sht30_monitor.py 의 동작을 STM32 + FreeRTOS 로 이식한다.
 *
 *  - 측정 태스크(sht30_measure_task):
 *      net 업 대기 -> SNTP 시간 동기 -> 주기적으로 SHT30 측정 후 서버 보고.
 *      sht30_monitor.py 의 first_success 로직과 동일하게, "최초 성공" 측정은
 *      event_type="startup", 이후는 "periodic" 으로 보고한다.
 *      온습도가 정상 범위를 벗어나면 metric_status="out_of_range" 로 분류한다.
 *      측정 실패 시 크래시하지 않고 로그만 남긴 뒤 다음 주기에 재시도한다.
 *
 *  - 헬스 태스크(health_task):
 *      IWDG 워치독을 주기적으로 refresh 하고, 태스크 스택 여유를 로깅한다.
 *      (RPi systemd watchdog / 헬스 체크 대응)
 *
 * 두 태스크의 분리 이유:
 *   네트워크 I/O(TLS 핸드셰이크/전송)는 수 초간 블로킹될 수 있으므로, 워치독
 *   refresh 를 측정 태스크에 의존시키면 통신 지연이 곧 리셋으로 이어질 수 있다.
 *   따라서 워치독은 독립적인 고우선순위 헬스 태스크에서 갱신한다.
 * ===========================================================================*/
#include <stdint.h>
#include <math.h>

#include "FreeRTOS.h"
#include "task.h"

#include "app_config.h"     /* 주기/범위/버전 + board_config.h + secrets.h */
#include "applog.h"
#include "watchdog.h"
#include "net.h"
#include "timesync.h"
#include "sht30.h"
#include "reporter.h"
#include "bsp.h"

/* ── 내부 타이밍 상수 ─────────────────────────────────────────────────────── */

/* net 업 / 시간 동기 대기 타임아웃. 실패해도 무한 재시도(폐쇄망 부팅 대비). */
#define APP_NET_WAIT_TIMEOUT_MS     (APP_NET_DHCP_TIMEOUT_MS + 10000u)
#define APP_TIMESYNC_TIMEOUT_MS     30000u

/* 헬스(워치독) 태스크 주기. 워치독 타임아웃의 1/4 이하로 충분히 여유 있게. */
#define HEALTH_TICK_MS              (APP_WATCHDOG_TIMEOUT_MS / 4u)

/* 측정 실패 후 짧은 재시도 간격 (정상 주기와 별도). 센서 글리치 복구용. */
#define MEASURE_RETRY_MS            5000u

/* 스택 여유 로깅 주기 (헬스 틱 N회마다 1번). */
#define STACK_LOG_EVERY_N_TICKS     12u

/* 측정 태스크 핸들 (스택 워터마크 로깅용으로 헬스 태스크가 참조) */
static TaskHandle_t s_measure_task = NULL;

/* ── 헬퍼: 온습도 정상 범위 분류 (sht30_monitor.py metric_status) ─────────── */
static const char *classify_metric_status(double temp_c, double rh)
{
    /* NaN/Inf 방어: 비정상 부동소수도 out_of_range 로 취급 */
    if (!isfinite(temp_c) || !isfinite(rh)) {
        return "out_of_range";
    }
    if (temp_c < APP_SHT30_TEMP_LOW_C || temp_c > APP_SHT30_TEMP_HIGH_C) {
        return "out_of_range";
    }
    if (rh < APP_SHT30_RH_LOW || rh > APP_SHT30_RH_HIGH) {
        return "out_of_range";
    }
    return "normal";
}

/* ── 헬퍼: 네트워크 업 + 시간 동기 보장 (무한 재시도) ─────────────────────── */
static void ensure_network_and_time(void)
{
    /* 1) 링크 업 + IP 확보까지 대기. 폐쇄망에서 케이블/스위치 지연이 있을 수
     *    있으므로 타임아웃 시 경고만 남기고 계속 재시도한다. */
    while (net_wait_up(APP_NET_WAIT_TIMEOUT_MS) != 0) {
        LOGW("net not up yet, retrying...");
        /* 워치독은 헬스 태스크가 별도로 refresh 하므로 여기서는 단순 대기 */
        vTaskDelay(pdMS_TO_TICKS(2000));
    }

    char ip[16];
    net_ip_str(ip, sizeof(ip));
    LOGI("network up, ip=%s", ip);

    /* 2) SNTP 시작 후 최초 동기까지 대기. TLS 인증서 유효기간 검증과 보고
     *    timestamp 필드에 정확한 Unix 시간이 필요하므로 동기 완료를 보장한다.
     *    (SNTP 는 UDP 라 TLS 불필요 -> TLS 연결보다 먼저 수행) */
    timesync_init();
    while (timesync_wait(APP_TIMESYNC_TIMEOUT_MS) != 0) {
        LOGW("time sync pending, retrying...");
        vTaskDelay(pdMS_TO_TICKS(APP_SNTP_RETRY_DELAY_MS));
    }
    LOGI("time synced, epoch=%lu", (unsigned long)timesync_now());
}

/* ── 측정 태스크 ─────────────────────────────────────────────────────────── */
/* RPi sht30_monitor.py main loop 대응:
 *   while True:
 *     reading = read_sht30()
 *     if reading is None: 로그 후 continue
 *     event_type = "startup" if first_success else "periodic"
 *     post_reading(...); first_success = False
 *     sleep(REPORT_INTERVAL)
 */
static void sht30_measure_task(void *arg)
{
    (void)arg;

    /* 부팅 시 네트워크/시간 준비 (sht30_monitor.py 의 시작 전 OS NTP 대응) */
    ensure_network_and_time();

    /* first_success: 첫 성공 측정만 "startup" 으로 보고 (RPi 동일 로직) */
    int first_success = 1;

    LOGI("sht30 measure task started (interval=%us)",
         (unsigned)APP_SHT30_REPORT_INTERVAL_SEC);

    for (;;) {
        double temp_c = 0.0;
        double rh = 0.0;

        int rc = sht30_measure(&temp_c, &rh);
        if (rc != 0) {
            /* 측정 실패: 크래시 금지. 로그 후 짧게 쉬고 다음 주기에 재시도.
             *   rc == -1: I2C 통신 오류, rc == -2: CRC 오류 */
            LOGE("sht30_measure failed (rc=%d), retry in %ums",
                 rc, (unsigned)MEASURE_RETRY_MS);
            vTaskDelay(pdMS_TO_TICKS(MEASURE_RETRY_MS));
            continue;
        }

        const char *metric_status = classify_metric_status(temp_c, rh);
        const char *event_type = first_success ? "startup" : "periodic";

        /* 운영 가시성: 측정값/상태 로깅 (소수 2자리로 표시) */
        LOGI("sht30 %s: T=%.2fC RH=%.2f%% status=%s",
             event_type, temp_c, rh, metric_status);

        /* 서버 보고. reporter 가 본문 생성 -> 서명 -> TLS POST -> 재시도 처리.
         * 반환 0 성공(HTTP 200), 음수 실패. */
        int prc = report_sht30_event(event_type, temp_c, rh, metric_status);
        if (prc == 0) {
            /* 최초 성공 보고 이후에는 startup -> periodic 으로 전환 */
            if (first_success) {
                first_success = 0;
            }
            bsp_led_toggle();   /* 성공 보고 표시 */
        } else {
            /* 보고 실패: reporter 내부 재시도까지 모두 소진한 경우.
             * 다음 정상 주기에 다시 시도한다. first_success 는 유지하여
             * 최초 보고가 반드시 "startup" 으로 도달하도록 한다. */
            LOGW("report_sht30_event failed (rc=%d), will retry next cycle", prc);
        }

        /* 정상 보고 주기 대기 (sht30_monitor.py REPORT_INTERVAL). */
        vTaskDelay(pdMS_TO_TICKS((TickType_t)APP_SHT30_REPORT_INTERVAL_SEC * 1000u));
    }
}

/* ── 헬스 태스크 ─────────────────────────────────────────────────────────── */
/* 워치독 refresh + 주기적 스택 여유 로깅. 네트워크/측정 블로킹과 독립적으로
 * 동작해야 하므로 측정 태스크보다 높은 우선순위로 생성된다(main.c). */
static void health_task(void *arg)
{
    (void)arg;

    uint32_t tick = 0;

    for (;;) {
        /* IWDG 갱신: 이 호출이 APP_WATCHDOG_TIMEOUT_MS 내에 끊기면 MCU 리셋 */
        watchdog_refresh();

        /* 주기적 스택 워터마크 로깅 (개발/운영 진단용) */
        if ((tick % STACK_LOG_EVERY_N_TICKS) == 0u) {
            UBaseType_t health_free = uxTaskGetStackHighWaterMark(NULL);
            UBaseType_t measure_free = (s_measure_task != NULL)
                ? uxTaskGetStackHighWaterMark(s_measure_task)
                : 0;
            LOGI("health: heap_free=%u stack_min[health]=%u stack_min[measure]=%u",
                 (unsigned)xPortGetFreeHeapSize(),
                 (unsigned)health_free,
                 (unsigned)measure_free);
        }

        tick++;
        vTaskDelay(pdMS_TO_TICKS(HEALTH_TICK_MS));
    }
}

/* ── 태스크 생성 진입점 (main.c 에서 호출) ───────────────────────────────── */
/* 스택 크기: TLS 핸드셰이크는 mbedTLS 가 측정 태스크 문맥에서 수행되므로
 * 측정 태스크에 넉넉한 스택을 준다(reporter 가 동일 태스크에서 호출됨). */
#define MEASURE_TASK_STACK_WORDS    3072u   /* 12 KB (word=4B): mbedTLS 핸드셰이크 피크 여유 */
#define HEALTH_TASK_STACK_WORDS      256u   /* 1 KB: 로깅/워치독만 */

/* 우선순위: 헬스(워치독) > 측정. 워치독 갱신이 통신 지연에 막히지 않게 한다. */
#define MEASURE_TASK_PRIO           (tskIDLE_PRIORITY + 2)
#define HEALTH_TASK_PRIO            (tskIDLE_PRIORITY + 3)

void app_sht30_start(void)
{
    BaseType_t ok;

    ok = xTaskCreate(sht30_measure_task, "sht30",
                     MEASURE_TASK_STACK_WORDS, NULL,
                     MEASURE_TASK_PRIO, &s_measure_task);
    if (ok != pdPASS) {
        bsp_fatal("failed to create sht30 measure task");
    }

    ok = xTaskCreate(health_task, "health",
                     HEALTH_TASK_STACK_WORDS, NULL,
                     HEALTH_TASK_PRIO, NULL);
    if (ok != pdPASS) {
        bsp_fatal("failed to create health task");
    }
}
