/* =============================================================================
 * timesync.c - SNTP 시간 동기 + RTC 반영 (timesync.h 구현)
 *
 * 동작:
 *   - timesync_init(): LwIP apps/sntp 를 POLL 모드로 시작(서버=APP_SNTP_SERVER).
 *   - SNTP 응답 수신 시 lwipopts.h 의 SNTP_SET_SYSTEM_TIME(sec) 매크로가
 *     timesync_apply_epoch() 를 호출 -> Unix 초를 분해해 HAL_RTC_Set{Time,Date}.
 *   - timesync_now(): RTC 를 다시 읽어 Unix 초로 환산. (RTC 미설정 시 0)
 *
 * 시간대: 모든 시간은 UTC 로 다룬다. 서버 timestamp / TLS 인증서 유효기간 검증
 *        모두 UTC 기준. RTC 에도 UTC 를 저장한다.
 *
 * RTC 클럭소스: 기본 LSI(저정밀, 외부 부품 불필요). 보드에 32.768kHz LSE 가
 *              있으면 정밀도가 좋다. TODO(hw): 실제 보드에 맞춰 bsp 에서 RTC 클럭
 *              소스를 LSE 로 전환(아래 RTC 초기화 가드 참고).
 * ===========================================================================*/
#include "timesync.h"
#include "app_config.h"
#include "applog.h"

#include "stm32f4xx_hal.h"

/* LwIP SNTP 앱 */
#include "lwip/apps/sntp.h"

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

#include <string.h>

/* ── 내부 상태 ──────────────────────────────────────────────────────────── */

/* HAL RTC 핸들. bsp 가 RTC 클럭/도메인을 켠 뒤 여기서 Init 한다. */
static RTC_HandleTypeDef s_hrtc;

/* 한 번이라도 SNTP 동기가 끝났는지(volatile: 콜백/태스크 경합). */
static volatile int s_is_set = 0;

/* RTC 가 HAL_RTC_Init 까지 완료됐는지. */
static int s_rtc_ready = 0;

/* RTC 연도 기준(STM32 RTC 는 2자리 연도 BCD: 0..99). 2000년 기준 사용. */
#define TS_RTC_YEAR_BASE   2000
/* RTC 가 표현 가능한 최소 Unix epoch(2000-01-01 00:00:00 UTC). */
#define TS_EPOCH_2000      946684800UL

/* ── 시간 분해/합성 유틸 (gmtime/timegm 대체, UTC 고정) ─────────────────── */

static int ts_is_leap(int year)
{
    return ((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0);
}

/* Unix 초(UTC) -> 분해 시각. 윤년/월일 정확. */
typedef struct {
    int year;   /* 절대 연도, 예: 2026 */
    int mon;    /* 1..12 */
    int mday;   /* 1..31 */
    int hour;   /* 0..23 */
    int min;    /* 0..59 */
    int sec;    /* 0..59 */
    int wday;   /* 1..7, 월=1 ... 일=7 (HAL RTC_WeekDay 호환) */
} ts_tm_t;

static void ts_epoch_to_tm(uint32_t epoch, ts_tm_t *out)
{
    static const int mdays[12] = { 31,28,31,30,31,30,31,31,30,31,30,31 };

    uint32_t days = epoch / 86400UL;
    uint32_t rem  = epoch % 86400UL;

    out->hour = (int)(rem / 3600UL);
    rem %= 3600UL;
    out->min = (int)(rem / 60UL);
    out->sec = (int)(rem % 60UL);

    /* 1970-01-01 은 목요일(Thursday). HAL 의 RTC_WEEKDAY: 월=1..일=7.
     * Unix wday(일=0..토=6) -> ISO(월=1..일=7) 로 변환. */
    int unix_wday = (int)((days + 4) % 7);          /* 0=Sun..6=Sat */
    out->wday = (unix_wday == 0) ? 7 : unix_wday;   /* 1=Mon..7=Sun */

    int year = 1970;
    while (1) {
        int ydays = ts_is_leap(year) ? 366 : 365;
        if (days >= (uint32_t)ydays) {
            days -= (uint32_t)ydays;
            year++;
        } else {
            break;
        }
    }
    out->year = year;

    int mon = 0;
    while (mon < 12) {
        int dm = mdays[mon];
        if (mon == 1 && ts_is_leap(year)) dm = 29;
        if (days >= (uint32_t)dm) {
            days -= (uint32_t)dm;
            mon++;
        } else {
            break;
        }
    }
    out->mon  = mon + 1;
    out->mday = (int)days + 1;
}

/* 분해 시각(UTC) -> Unix 초. */
static uint32_t ts_tm_to_epoch(int year, int mon, int mday,
                               int hour, int min, int sec)
{
    static const int mdays[12] = { 31,28,31,30,31,30,31,31,30,31,30,31 };
    uint32_t days = 0;

    for (int y = 1970; y < year; y++) {
        days += ts_is_leap(y) ? 366 : 365;
    }
    for (int m = 0; m < (mon - 1); m++) {
        int dm = mdays[m];
        if (m == 1 && ts_is_leap(year)) dm = 29;
        days += (uint32_t)dm;
    }
    days += (uint32_t)(mday - 1);

    return days * 86400UL + (uint32_t)hour * 3600UL
         + (uint32_t)min * 60UL + (uint32_t)sec;
}

/* ── RTC 초기화 ─────────────────────────────────────────────────────────── */

/* RTC 페리페럴 초기화(1회). 클럭소스 선택 + 비동기/동기 프리스케일러로 1Hz.
 * 반환 0 성공, 음수 실패. */
static int ts_rtc_init(void)
{
    if (s_rtc_ready) {
        return 0;
    }

    /* RTC 백업도메인 접근/클럭소스 설정.
     * 기본은 LSI(~32kHz, 부품 불필요). LSE 사용 시 아래 가드를 LSE 로 전환.
     * TODO(hw): 보드에 32.768kHz 크리스털이 있으면 LSE 경로를 활성화. */
    RCC_OscInitTypeDef        osc  = {0};
    RCC_PeriphCLKInitTypeDef  pclk = {0};

    /* 백업도메인 쓰기 보호 해제(PWR 클럭은 bsp 에서 켜졌다고 가정하나, 안전하게 한 번 더). */
    __HAL_RCC_PWR_CLK_ENABLE();
    HAL_PWR_EnableBkUpAccess();

#if defined(TS_USE_LSE) /* TODO(hw): LSE 보드면 빌드에 -DTS_USE_LSE 추가 */
    osc.OscillatorType = RCC_OSCILLATORTYPE_LSE;
    osc.PLL.PLLState   = RCC_PLL_NONE;
    osc.LSEState       = RCC_LSE_ON;
    if (HAL_RCC_OscConfig(&osc) != HAL_OK) {
        return -1;
    }
    pclk.PeriphClockSelection = RCC_PERIPHCLK_RTC;
    pclk.RTCClockSelection    = RCC_RTCCLKSOURCE_LSE;
    if (HAL_RCCEx_PeriphCLKConfig(&pclk) != HAL_OK) {
        return -2;
    }
    /* LSE 32768Hz -> async(127)+1, sync(255)+1 => 1Hz */
    const uint32_t async_pre = 127;
    const uint32_t sync_pre  = 255;
#else
    osc.OscillatorType = RCC_OSCILLATORTYPE_LSI;
    osc.PLL.PLLState   = RCC_PLL_NONE;
    osc.LSIState       = RCC_LSI_ON;
    if (HAL_RCC_OscConfig(&osc) != HAL_OK) {
        return -1;
    }
    pclk.PeriphClockSelection = RCC_PERIPHCLK_RTC;
    pclk.RTCClockSelection    = RCC_RTCCLKSOURCE_LSI;
    if (HAL_RCCEx_PeriphCLKConfig(&pclk) != HAL_OK) {
        return -2;
    }
    /* LSI 약 32000Hz -> async(124)+1=125, sync(255)+1=256 => 125*256=32000 -> 1Hz.
     * LSI 는 오차가 크므로 SNTP 가 주기적으로 보정한다(SNTP_UPDATE_DELAY). */
    const uint32_t async_pre = 124;
    const uint32_t sync_pre  = 255;
#endif

    __HAL_RCC_RTC_ENABLE();

    s_hrtc.Instance            = RTC;
    s_hrtc.Init.HourFormat     = RTC_HOURFORMAT_24;
    s_hrtc.Init.AsynchPrediv   = async_pre;
    s_hrtc.Init.SynchPrediv    = sync_pre;
    s_hrtc.Init.OutPut         = RTC_OUTPUT_DISABLE;
    s_hrtc.Init.OutPutPolarity = RTC_OUTPUT_POLARITY_HIGH;
    s_hrtc.Init.OutPutType     = RTC_OUTPUT_TYPE_OPENDRAIN;

    if (HAL_RTC_Init(&s_hrtc) != HAL_OK) {
        return -3;
    }

    s_rtc_ready = 1;
    return 0;
}

/* ── 공개 API ───────────────────────────────────────────────────────────── */

void timesync_init(void)
{
    /* RTC 를 먼저 준비(아직 시간은 미설정). SNTP 응답이 와야 실제 시간이 채워진다. */
    if (ts_rtc_init() != 0) {
        LOGE("timesync: RTC init failed (계속: SNTP 동기 시 재시도 안 함)");
        /* RTC 가 없어도 SNTP 자체는 동작하지만, 시간 보존 불가.
         * apply_epoch 에서 다시 init 을 시도한다. */
    }

    sntp_setoperatingmode(SNTP_OPMODE_POLL);
    sntp_setservername(0, APP_SNTP_SERVER);
    sntp_init();

    LOGI("timesync: SNTP 시작 server=%s", APP_SNTP_SERVER);
}

void timesync_apply_epoch(uint32_t unix_seconds)
{
    /* 비정상적으로 작은 값(2000년 이전)은 무시: 잘못된 SNTP 응답 방지. */
    if (unix_seconds < TS_EPOCH_2000) {
        LOGW("timesync: epoch 무시(%lu < 2000) ", (unsigned long)unix_seconds);
        return;
    }

    /* 콜백 컨텍스트(tcpip 스레드)에서 호출될 수 있음. RTC 미준비 시 1회 시도. */
    if (!s_rtc_ready) {
        if (ts_rtc_init() != 0) {
            /* RTC 사용 불가: 플래그만 세워 timesync_is_set 은 만족시키되,
             * timesync_now 는 RTC 미설정으로 0 을 줄 수 있음. */
            s_is_set = 1;
            return;
        }
    }

    ts_tm_t tm;
    ts_epoch_to_tm(unix_seconds, &tm);

    RTC_TimeTypeDef t = {0};
    RTC_DateTypeDef d = {0};

    t.Hours          = (uint8_t)tm.hour;
    t.Minutes        = (uint8_t)tm.min;
    t.Seconds        = (uint8_t)tm.sec;
    t.TimeFormat     = RTC_HOURFORMAT12_AM;     /* 24h 모드에선 무시됨 */
    t.DayLightSaving = RTC_DAYLIGHTSAVING_NONE;
    t.StoreOperation = RTC_STOREOPERATION_RESET;

    d.WeekDay = (uint8_t)tm.wday;               /* RTC_WEEKDAY_MONDAY=1 .. SUNDAY=7 */
    d.Month   = (uint8_t)tm.mon;                /* 1..12, BCD 변환은 HAL 이 처리(BIN 포맷) */
    d.Date    = (uint8_t)tm.mday;
    d.Year    = (uint8_t)(tm.year - TS_RTC_YEAR_BASE);   /* 0..99 */

    if (HAL_RTC_SetTime(&s_hrtc, &t, RTC_FORMAT_BIN) != HAL_OK ||
        HAL_RTC_SetDate(&s_hrtc, &d, RTC_FORMAT_BIN) != HAL_OK) {
        LOGE("timesync: RTC set 실패");
        /* 그래도 동기됐다고 표시: 최소한 한 번 시간이 들어왔음. */
        s_is_set = 1;
        return;
    }

    s_is_set = 1;
    LOGI("timesync: 동기 완료 epoch=%lu (%04d-%02d-%02d %02d:%02d:%02d UTC)",
         (unsigned long)unix_seconds,
         tm.year, tm.mon, tm.mday, tm.hour, tm.min, tm.sec);
}

uint32_t timesync_now(void)
{
    if (!s_is_set || !s_rtc_ready) {
        return 0;
    }

    /* HAL_RTC_GetTime 을 먼저 호출해야 섀도 레지스터가 잠기고,
     * 이후 GetDate 호출로 잠금이 풀린다(HAL 규약). 순서 준수 필수. */
    RTC_TimeTypeDef t = {0};
    RTC_DateTypeDef d = {0};

    if (HAL_RTC_GetTime(&s_hrtc, &t, RTC_FORMAT_BIN) != HAL_OK) {
        return 0;
    }
    if (HAL_RTC_GetDate(&s_hrtc, &d, RTC_FORMAT_BIN) != HAL_OK) {
        return 0;
    }

    int year = TS_RTC_YEAR_BASE + (int)d.Year;
    return ts_tm_to_epoch(year, (int)d.Month, (int)d.Date,
                          (int)t.Hours, (int)t.Minutes, (int)t.Seconds);
}

int timesync_is_set(void)
{
    return s_is_set ? 1 : 0;
}

int timesync_wait(uint32_t timeout_ms)
{
    const uint32_t step_ms = 100;
    uint32_t waited = 0;

    while (!s_is_set) {
        if (timeout_ms != 0 && waited >= timeout_ms) {
            return -1;
        }
        vTaskDelay(pdMS_TO_TICKS(step_ms));
        waited += step_ms;
    }
    return 0;
}
