/* =============================================================================
 * tls_mbedtls.c - mbedTLS(TLS 1.2 클라이언트) over LwIP 소켓 전송
 *
 * 역할:
 *   - tls.h 구현: tls_init() (부팅 1회), tls_transport_init() (transport_t 바인딩).
 *   - transport_t 의 connect/send/recv/close 를 mbedTLS + LwIP 소켓으로 제공.
 *   - 엔트로피: STM32 하드웨어 RNG → ctr_drbg 시드 (MBEDTLS_ENTROPY_HARDWARE_ALT).
 *   - 서버 인증서: certs/server_ca.h 의 임베드 루트 CA 로 풀 검증(VERIFY_REQUIRED).
 *   - SNI: mbedtls_ssl_set_hostname(host).
 *
 * 설계 메모:
 *   - 전역 1회 초기화(tls_init)에서 만든 entropy/ctr_drbg/CA(x509_crt) 는 모든
 *     연결이 공유한다(상주). ssl_config 와 ssl_context 는 연결마다 새로 만들고
 *     close 에서 해제한다(매 보고가 Connection: close 단발 연결이므로 단순/안전).
 *   - LwIP 소켓은 블로킹 모드 + SO_RCVTIMEO 로 타임아웃을 처리한다. mbedTLS BIO
 *     콜백은 EAGAIN/EWOULDBLOCK 을 MBEDTLS_ERR_SSL_WANT_READ/WRITE 로 매핑한다.
 *
 * 폐쇄망(air-gapped): mbedTLS/LwIP 는 third_party 에 벤더링. 런타임 외부 fetch 없음.
 *   인증서는 flash 임베드(런타임 CA 다운로드 불가).
 * ===========================================================================*/

#include "tls.h"
#include "app_config.h"
#include "applog.h"
#include "timesync.h"            /* timesync_now/timesync_is_set: 인증서 유효기간 검증용 시간 */
#include "certs/server_ca.h"

#include <string.h>
#include <time.h>                /* time_t (mbedTLS PLATFORM_TIME_MACRO 콜백) */

/* ── mbedTLS ─────────────────────────────────────────────────────────────── */
#include "mbedtls/ssl.h"
#include "mbedtls/entropy.h"
#include "mbedtls/ctr_drbg.h"
#include "mbedtls/x509_crt.h"
#include "mbedtls/error.h"
#include "mbedtls/platform.h"

/* ── LwIP 소켓 ───────────────────────────────────────────────────────────── */
/* 주의(net 에이전트 lwipopts.h 의존): LWIP_SOCKET=1, LWIP_DNS=1, LWIP_NETCONN=1,
 * LWIP_SO_RCVTIMEO=1, LWIP_SO_SNDTIMEO=1, LWIP_DNS_API_DECLARE_STRUCTS=1(getaddrinfo)
 * 이 켜져 있어야 한다(SO_RCVTIMEO/getaddrinfo 사용). */
#include "lwip/sockets.h"
#include "lwip/netdb.h"
/* errno 값(EWOULDBLOCK/EAGAIN/ECONNRESET 등)은 LwIP 소켓 계층이 set_errno() 로
 * 설정하므로, 표준 <errno.h> 가 아니라 LwIP 의 정의를 사용해 값 일치를 보장한다.
 * (lwipopts.h 에서 LWIP_PROVIDE_ERRNO=1 또는 시스템 errno 매핑 둘 다 호환) */
#include "lwip/errno.h"

/* ── STM32 HAL (하드웨어 RNG) ────────────────────────────────────────────── */
#include "stm32f4xx_hal.h"

/* ── FreeRTOS 힙 (플랫폼 calloc/free 매핑) ───────────────────────────────── */
#include "FreeRTOS.h"
#include "task.h"

/* =============================================================================
 * 0) 플랫폼 메모리: mbedtls calloc/free → FreeRTOS heap
 *    mbedtls_config.h 가 CALLOC_MACRO/FREE_MACRO 로 이 두 함수를 가리킨다.
 * ===========================================================================*/
void *mbedtls_platform_calloc(size_t n, size_t size)
{
    size_t total = n * size;
    /* 곱셈 오버플로 방어 (n,size 둘 다 0 이 아니면 검사) */
    if (n != 0 && total / n != size) {
        return NULL;
    }
    void *p = pvPortMalloc(total);
    if (p != NULL) {
        memset(p, 0, total);   /* calloc 계약: 0 초기화 */
    }
    return p;
}

void mbedtls_platform_free(void *p)
{
    if (p != NULL) {
        vPortFree(p);
    }
}

/* =============================================================================
 * 0b) 플랫폼 시간: mbedtls_time → RTC/SNTP 기반 Unix 시간
 *    mbedtls_config.h 가 MBEDTLS_PLATFORM_TIME_MACRO 로 이 함수를 가리킨다.
 *    X.509 인증서 notBefore/notAfter 검증(MBEDTLS_HAVE_TIME_DATE)에 사용된다.
 *    시간 미동기 시 0 을 반환하지만, tls_connect() 가 timesync_is_set() 으로
 *    선제 차단하므로 0 시각으로 검증이 수행되는 일은 없다(이중 안전장치).
 * ===========================================================================*/
time_t tls_platform_time(time_t *t)
{
    time_t now = (time_t)timesync_now();
    if (t != NULL) {
        *t = now;
    }
    return now;
}

/* =============================================================================
 * 1) 하드웨어 RNG (STM32 RNG 주변장치) + 엔트로피 폴 콜백
 *    mbedtls_config.h: MBEDTLS_ENTROPY_HARDWARE_ALT → 아래 콜백을 entropy 가 호출.
 * ===========================================================================*/
static RNG_HandleTypeDef s_hrng;     /* TLS 모듈이 소유하는 RNG 핸들 */
static int s_rng_ready = 0;

/* RNG 주변장치 초기화 (tls_init 에서 1회). 0 성공, 음수 실패. */
static int hw_rng_init(void)
{
    if (s_rng_ready) {
        return 0;
    }
    __HAL_RCC_RNG_CLK_ENABLE();      /* RNG 클럭 게이팅 ON */

    s_hrng.Instance = RNG;
    if (HAL_RNG_Init(&s_hrng) != HAL_OK) {
        return -1;
    }
    s_rng_ready = 1;
    return 0;
}

/* mbedTLS 하드웨어 엔트로피 소스.
 * RNG 로 32비트 난수를 반복 생성하여 output[0..len) 을 채운다.
 * 반환: 0 성공(*olen=len), 음수 실패. */
int mbedtls_hardware_poll(void *data, unsigned char *output, size_t len, size_t *olen)
{
    (void)data;

    if (!s_rng_ready) {
        if (hw_rng_init() != 0) {
            return MBEDTLS_ERR_ENTROPY_SOURCE_FAILED;
        }
    }

    size_t produced = 0;
    while (produced < len) {
        uint32_t rnd = 0;
        if (HAL_RNG_GenerateRandomNumber(&s_hrng, &rnd) != HAL_OK) {
            /* RNG 클럭/시드 오류(CEIS/SEIS). 핸들 재초기화 후 1회 재시도하지 않고
             * 즉시 실패 보고 — 엔트로피 품질을 절대 가장하지 않는다. */
            return MBEDTLS_ERR_ENTROPY_SOURCE_FAILED;
        }
        size_t chunk = len - produced;
        if (chunk > sizeof(rnd)) {
            chunk = sizeof(rnd);
        }
        memcpy(output + produced, &rnd, chunk);
        produced += chunk;
    }

    *olen = produced;
    return 0;
}

/* =============================================================================
 * 2) 전역 TLS 상태 (모든 연결 공유, 상주)
 * ===========================================================================*/
static mbedtls_entropy_context  s_entropy;
static mbedtls_ctr_drbg_context s_ctr_drbg;
static mbedtls_x509_crt         s_ca_chain;
static int                      s_tls_inited = 0;

/* ctr_drbg 개인화 라벨 (선택적 추가 엔트로피) */
static const char *const TLS_DRBG_PERS = "sht30-sms-stm32-tls";

/* mbedTLS 오류 코드를 사람이 읽는 문자열로 로그 (MBEDTLS_ERROR_C 활성 시). */
static void log_mbedtls_err(const char *what, int ret)
{
#if defined(MBEDTLS_ERROR_C)
    char buf[96];
    mbedtls_strerror(ret, buf, sizeof(buf));
    LOGE("TLS %s: -0x%04X (%s)", what, (unsigned)(-ret), buf);
#else
    LOGE("TLS %s: -0x%04X", what, (unsigned)(-ret));
#endif
}

int tls_init(void)
{
    if (s_tls_inited) {
        return 0;   /* 멱등 */
    }

    /* 플랫폼 메모리 콜백은 CALLOC_MACRO/FREE_MACRO 로 정적 바인딩되어 별도 set 불필요.
     * (MBEDTLS_PLATFORM_MEMORY + *_MACRO 사용) */

    /* 하드웨어 RNG 먼저 준비 (엔트로피 폴 콜백이 사용). */
    if (hw_rng_init() != 0) {
        LOGE("TLS hw_rng_init failed");
        return -1;
    }

    mbedtls_entropy_init(&s_entropy);
    mbedtls_ctr_drbg_init(&s_ctr_drbg);
    mbedtls_x509_crt_init(&s_ca_chain);

    /* DRBG 시드: entropy(하드웨어 RNG ALT) 를 소스로 사용. */
    int ret = mbedtls_ctr_drbg_seed(
        &s_ctr_drbg, mbedtls_entropy_func, &s_entropy,
        (const unsigned char *)TLS_DRBG_PERS, strlen(TLS_DRBG_PERS));
    if (ret != 0) {
        log_mbedtls_err("ctr_drbg_seed", ret);
        goto fail;
    }

    /* 임베드 루트 CA(PEM) 파싱. PEM 은 buflen 에 종결 NUL 포함 필수
     * (server_ca.h 계약: SERVER_CA_PEM_LEN == sizeof(SERVER_CA_PEM)). */
    ret = mbedtls_x509_crt_parse(
        &s_ca_chain, (const unsigned char *)SERVER_CA_PEM, SERVER_CA_PEM_LEN);
    if (ret != 0) {
        /* ret > 0 이면 일부 인증서 파싱 실패 개수. 어느 경우든 신뢰체인 불완전. */
        log_mbedtls_err("x509_crt_parse(CA)", ret);
        goto fail;
    }

    s_tls_inited = 1;
    LOGI("TLS init ok (CA parsed, DRBG seeded via HW RNG)");
    return 0;

fail:
    mbedtls_x509_crt_free(&s_ca_chain);
    mbedtls_ctr_drbg_free(&s_ctr_drbg);
    mbedtls_entropy_free(&s_entropy);
    return -2;
}

/* =============================================================================
 * 3) 연결별 컨텍스트 (transport_t.ctx 가 가리킴)
 * ===========================================================================*/
typedef struct {
    mbedtls_ssl_context  ssl;
    mbedtls_ssl_config   conf;
    int                  fd;        /* LwIP 소켓 fd (-1 = 미연결) */
    int                  in_use;    /* 진행 중 연결 여부 */
} tls_ctx_t;

/* 단일 동시 TLS 연결 가정(보고는 단발). 정적 1개로 RAM 고정. 필요 시 풀로 확장. */
static tls_ctx_t s_tls_ctx;

/* ── BIO 콜백: LwIP 소켓 send/recv ↔ mbedTLS WANT_* 매핑 ────────────────── */
static int bio_send(void *vctx, const unsigned char *buf, size_t len)
{
    tls_ctx_t *c = (tls_ctx_t *)vctx;
    if (c->fd < 0) {
        return MBEDTLS_ERR_NET_INVALID_CONTEXT;
    }
    int n = (int)lwip_send(c->fd, buf, len, 0);
    if (n >= 0) {
        return n;
    }
    if (errno == EWOULDBLOCK || errno == EAGAIN || errno == EINTR) {
        return MBEDTLS_ERR_SSL_WANT_WRITE;
    }
    if (errno == EPIPE || errno == ECONNRESET) {
        return MBEDTLS_ERR_NET_CONN_RESET;
    }
    return MBEDTLS_ERR_NET_SEND_FAILED;
}

static int bio_recv(void *vctx, unsigned char *buf, size_t len)
{
    tls_ctx_t *c = (tls_ctx_t *)vctx;
    if (c->fd < 0) {
        return MBEDTLS_ERR_NET_INVALID_CONTEXT;
    }
    int n = (int)lwip_recv(c->fd, buf, len, 0);
    if (n > 0) {
        return n;
    }
    if (n == 0) {
        return 0;   /* 원격 종료 — mbedTLS 는 0 을 정상 종료로 처리 */
    }
    if (errno == EWOULDBLOCK || errno == EAGAIN || errno == EINTR) {
        /* SO_RCVTIMEO 만료도 여기로 온다(EWOULDBLOCK). 상위 핸드셰이크/read 루프가
         * 자체 타임아웃을 관리하므로 WANT_READ 로 재시도 신호를 준다. */
        return MBEDTLS_ERR_SSL_WANT_READ;
    }
    if (errno == ECONNRESET) {
        return MBEDTLS_ERR_NET_CONN_RESET;
    }
    return MBEDTLS_ERR_NET_RECV_FAILED;
}

/* ── 소켓 정리 헬퍼 ──────────────────────────────────────────────────────── */
static void close_socket(tls_ctx_t *c)
{
    if (c->fd >= 0) {
        lwip_close(c->fd);
        c->fd = -1;
    }
}

/* DNS 해석 + TCP connect. 0 성공, 음수 실패. fd 는 c->fd 에 저장. */
static int tcp_connect(tls_ctx_t *c, const char *host, uint16_t port, uint32_t timeout_ms)
{
    char portstr[6];
    /* uint16_t 포트 → 문자열 (최대 "65535") */
    {
        unsigned p = port;
        int idx = 5;
        portstr[idx] = '\0';
        if (p == 0) {
            portstr[--idx] = '0';
        } else {
            while (p > 0 && idx > 0) {
                portstr[--idx] = (char)('0' + (p % 10));
                p /= 10;
            }
        }
        /* 좌측 정렬로 복사 */
        memmove(portstr, portstr + idx, (size_t)(6 - idx));
    }

    struct addrinfo hints;
    struct addrinfo *res = NULL;
    memset(&hints, 0, sizeof(hints));
    hints.ai_family   = AF_INET;       /* IPv4 (LAN8720 + LwIP 기본 구성) */
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_protocol = IPPROTO_TCP;

    int gai = lwip_getaddrinfo(host, portstr, &hints, &res);
    if (gai != 0 || res == NULL) {
        LOGE("TLS DNS resolve failed for %s (gai=%d)", host, gai);
        return -1;
    }

    int fd = lwip_socket(res->ai_family, res->ai_socktype, res->ai_protocol);
    if (fd < 0) {
        LOGE("TLS socket() failed");
        lwip_freeaddrinfo(res);
        return -2;
    }

    /* 수신/송신 타임아웃 설정 (블로킹 + SO_*TIMEO). */
    struct timeval tv;
    tv.tv_sec  = (long)(timeout_ms / 1000u);
    tv.tv_usec = (long)((timeout_ms % 1000u) * 1000u);
    lwip_setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
    lwip_setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));

    if (lwip_connect(fd, res->ai_addr, res->ai_addrlen) != 0) {
        LOGE("TLS TCP connect failed to %s:%u", host, (unsigned)port);
        lwip_close(fd);
        lwip_freeaddrinfo(res);
        return -3;
    }

    lwip_freeaddrinfo(res);
    c->fd = fd;
    return 0;
}

/* =============================================================================
 * 4) transport_t 콜백 구현
 * ===========================================================================*/

/* connect: TCP 연결 → SSL config/setup → 핸드셰이크(+ 인증서 검증). */
static int tls_connect(void *vctx, const char *host, uint16_t port, uint32_t timeout_ms)
{
    tls_ctx_t *c = (tls_ctx_t *)vctx;
    int ret;

    if (!s_tls_inited) {
        LOGE("TLS connect before tls_init()");
        return -1;
    }
    /* 시간이 동기되지 않았으면 인증서 유효기간(notBefore/notAfter) 검증이
     * 무의미해진다(시각 0 이면 모든 인증서가 '아직 유효하지 않음'으로도 보임).
     * 보고 경로는 timesync_wait 로 동기 후 호출하지만, 방어적으로 차단한다. */
    if (!timesync_is_set()) {
        LOGE("TLS connect before time sync (cert validity check would be unsafe)");
        return -1;
    }
    if (c->in_use) {
        /* 이전 연결이 정리되지 않았다면 강제 정리 후 진행. */
        mbedtls_ssl_free(&c->ssl);
        mbedtls_ssl_config_free(&c->conf);
        close_socket(c);
        c->in_use = 0;
    }

    c->fd = -1;
    mbedtls_ssl_init(&c->ssl);
    mbedtls_ssl_config_init(&c->conf);

    /* (1) TCP 연결 + DNS */
    ret = tcp_connect(c, host, port, timeout_ms);
    if (ret != 0) {
        goto fail_cleanup;
    }

    /* (2) SSL config: TLS1.2 클라이언트, 서버 인증 필수, CA, RNG */
    ret = mbedtls_ssl_config_defaults(&c->conf,
                                      MBEDTLS_SSL_IS_CLIENT,
                                      MBEDTLS_SSL_TRANSPORT_STREAM,
                                      MBEDTLS_SSL_PRESET_DEFAULT);
    if (ret != 0) {
        log_mbedtls_err("config_defaults", ret);
        goto fail_cleanup;
    }

    mbedtls_ssl_conf_authmode(&c->conf, MBEDTLS_SSL_VERIFY_REQUIRED);
    mbedtls_ssl_conf_ca_chain(&c->conf, &s_ca_chain, NULL);
    mbedtls_ssl_conf_rng(&c->conf, mbedtls_ctr_drbg_random, &s_ctr_drbg);

    /* TLS 1.2 만 허용 (min == max). mbedTLS 3.x API.
     * (config 에 MBEDTLS_SSL_PROTO_TLS1_3 미정의이므로 빌드 자체가 TLS1.2-only 이나,
     *  명시적으로 한 번 더 고정한다.) */
    mbedtls_ssl_conf_min_tls_version(&c->conf, MBEDTLS_SSL_VERSION_TLS1_2);
    mbedtls_ssl_conf_max_tls_version(&c->conf, MBEDTLS_SSL_VERSION_TLS1_2);

#if defined(MBEDTLS_SSL_MAX_FRAGMENT_LENGTH)
    /* RAM 절감: 최대 단편 길이 협상(서버 지원 시). IN_CONTENT_LEN 4096 에 맞춤. */
    mbedtls_ssl_conf_max_frag_len(&c->conf, MBEDTLS_SSL_MAX_FRAG_LEN_4096);
#endif

    /* (3) SSL setup */
    ret = mbedtls_ssl_setup(&c->ssl, &c->conf);
    if (ret != 0) {
        log_mbedtls_err("ssl_setup", ret);
        goto fail_cleanup;
    }

    /* (4) SNI: 서버 호스트명 (인증서 CN/SAN 검증에도 사용) */
    ret = mbedtls_ssl_set_hostname(&c->ssl, host);
    if (ret != 0) {
        log_mbedtls_err("set_hostname", ret);
        goto fail_cleanup;
    }

    /* (5) BIO: LwIP 소켓 send/recv 콜백 바인딩 */
    mbedtls_ssl_set_bio(&c->ssl, c, bio_send, bio_recv, NULL);

    /* (6) 핸드셰이크 (WANT_READ/WRITE 재시도). 자체 타임아웃: 벽시계 기반. */
    {
        uint32_t start = (uint32_t)xTaskGetTickCount() * (uint32_t)portTICK_PERIOD_MS;
        for (;;) {
            ret = mbedtls_ssl_handshake(&c->ssl);
            if (ret == 0) {
                break;   /* 핸드셰이크 완료 */
            }
            if (ret != MBEDTLS_ERR_SSL_WANT_READ &&
                ret != MBEDTLS_ERR_SSL_WANT_WRITE) {
                log_mbedtls_err("handshake", ret);
                goto fail_cleanup;
            }
            /* WANT_* : 소켓 타임아웃(SO_RCVTIMEO)이 이미 블로킹을 제한한다.
             * 전체 핸드셰이크 상한도 별도로 강제한다. */
            uint32_t now = (uint32_t)xTaskGetTickCount() * (uint32_t)portTICK_PERIOD_MS;
            if ((now - start) >= timeout_ms) {
                LOGE("TLS handshake timeout (%ums)", (unsigned)timeout_ms);
                ret = -1;
                goto fail_cleanup;
            }
        }
    }

    /* (7) 인증서 검증 결과 확인 (VERIFY_REQUIRED 면 핸드셰이크가 이미 실패하지만,
     *     진단을 위해 명시적으로 로그한다). */
    {
        uint32_t flags = mbedtls_ssl_get_verify_result(&c->ssl);
        if (flags != 0) {
#if defined(MBEDTLS_X509_REMOVE_INFO)
            LOGE("TLS cert verify failed: flags=0x%08X", (unsigned)flags);
#else
            char vbuf[160];
            mbedtls_x509_crt_verify_info(vbuf, sizeof(vbuf), "  ! ", flags);
            LOGE("TLS cert verify failed (0x%08X):\n%s", (unsigned)flags, vbuf);
#endif
            ret = -1;
            goto fail_cleanup;
        }
    }

    c->in_use = 1;
    LOGI("TLS connected: %s:%u (%s, %s)",
         host, (unsigned)port,
         mbedtls_ssl_get_version(&c->ssl),
         mbedtls_ssl_get_ciphersuite(&c->ssl));
    return 0;

fail_cleanup:
    mbedtls_ssl_free(&c->ssl);
    mbedtls_ssl_config_free(&c->conf);
    close_socket(c);
    c->in_use = 0;
    return (ret < 0) ? ret : -100;
}

/* send: buf 전체 전송(부분 쓰기 루프). 전송 바이트(>0) / 음수 실패. */
static int tls_send(void *vctx, const uint8_t *buf, size_t len, uint32_t timeout_ms)
{
    tls_ctx_t *c = (tls_ctx_t *)vctx;
    if (!c->in_use) {
        return -1;
    }

    size_t sent = 0;
    uint32_t start = (uint32_t)xTaskGetTickCount() * (uint32_t)portTICK_PERIOD_MS;

    while (sent < len) {
        int ret = mbedtls_ssl_write(&c->ssl, buf + sent, len - sent);
        if (ret > 0) {
            sent += (size_t)ret;
            continue;
        }
        if (ret == MBEDTLS_ERR_SSL_WANT_READ || ret == MBEDTLS_ERR_SSL_WANT_WRITE) {
            uint32_t now = (uint32_t)xTaskGetTickCount() * (uint32_t)portTICK_PERIOD_MS;
            if ((now - start) >= timeout_ms) {
                LOGE("TLS send timeout");
                return -2;
            }
            continue;   /* 블로킹 소켓이 곧 진행시킨다 */
        }
        log_mbedtls_err("ssl_write", ret);
        return -3;
    }
    return (int)sent;
}

/* recv: 최대 cap 바이트 1회 수신. 바이트(>0) / 0 종료 / 음수 오류. */
static int tls_recv(void *vctx, uint8_t *buf, size_t cap, uint32_t timeout_ms)
{
    tls_ctx_t *c = (tls_ctx_t *)vctx;
    if (!c->in_use) {
        return -1;
    }
    if (cap == 0) {
        return 0;
    }

    uint32_t start = (uint32_t)xTaskGetTickCount() * (uint32_t)portTICK_PERIOD_MS;

    for (;;) {
        int ret = mbedtls_ssl_read(&c->ssl, buf, cap);
        if (ret > 0) {
            return ret;   /* 읽은 바이트 */
        }
        if (ret == 0 ||
            ret == MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY) {
            return 0;     /* 정상 종료 */
        }
        if (ret == MBEDTLS_ERR_SSL_WANT_READ || ret == MBEDTLS_ERR_SSL_WANT_WRITE) {
            uint32_t now = (uint32_t)xTaskGetTickCount() * (uint32_t)portTICK_PERIOD_MS;
            if ((now - start) >= timeout_ms) {
                LOGE("TLS recv timeout");
                return -2;
            }
            continue;
        }
        log_mbedtls_err("ssl_read", ret);
        return -3;
    }
}

/* close: close_notify → ssl/conf 해제 → 소켓 close. */
static void tls_close(void *vctx)
{
    tls_ctx_t *c = (tls_ctx_t *)vctx;
    if (!c->in_use) {
        /* 이미 정리됨 (또는 connect 실패). 소켓만 혹시 남아있으면 닫는다. */
        close_socket(c);
        return;
    }

    /* close_notify 는 best-effort. 죽은/멈춘 피어에서 무한 대기하지 않도록
     * 짧은 벽시계 상한(CLOSE_NOTIFY_MAX_MS)으로 묶는다. (블로킹 소켓은 매 시도마다
     * SO_SNDTIMEO 까지 블록되므로, 상한이 없으면 reporter 가 TLS 뮤텍스를 쥔 채
     * 여러 차례 길게 멈출 수 있다.) 상한 초과 시 그냥 해제/소켓 close 로 진행한다. */
    {
        const uint32_t CLOSE_NOTIFY_MAX_MS = 1000u;
        uint32_t start = (uint32_t)xTaskGetTickCount() * (uint32_t)portTICK_PERIOD_MS;
        int ret;
        do {
            ret = mbedtls_ssl_close_notify(&c->ssl);
            if (ret != MBEDTLS_ERR_SSL_WANT_READ && ret != MBEDTLS_ERR_SSL_WANT_WRITE) {
                break;   /* 완료 또는 (무시 가능한) 오류 */
            }
            uint32_t now = (uint32_t)xTaskGetTickCount() * (uint32_t)portTICK_PERIOD_MS;
            if ((now - start) >= CLOSE_NOTIFY_MAX_MS) {
                break;   /* 상한 초과 → best-effort 포기 */
            }
        } while (1);
    }

    mbedtls_ssl_free(&c->ssl);
    mbedtls_ssl_config_free(&c->conf);
    close_socket(c);
    c->in_use = 0;
}

/* =============================================================================
 * 5) transport_t 바인딩
 * ===========================================================================*/
int tls_transport_init(transport_t *t)
{
    if (t == NULL) {
        return -1;
    }
    if (!s_tls_inited) {
        LOGE("tls_transport_init before tls_init()");
        return -2;
    }

    /* 정적 연결 컨텍스트 초기 상태로 리셋(이전 잔여 정리). */
    if (s_tls_ctx.in_use) {
        mbedtls_ssl_free(&s_tls_ctx.ssl);
        mbedtls_ssl_config_free(&s_tls_ctx.conf);
    }
    close_socket(&s_tls_ctx);
    memset(&s_tls_ctx, 0, sizeof(s_tls_ctx));
    s_tls_ctx.fd = -1;

    t->ctx     = &s_tls_ctx;
    t->connect = tls_connect;
    t->send    = tls_send;
    t->recv    = tls_recv;
    t->close   = tls_close;
    return 0;
}
