/*
 * kl_session.c
 * Session layer: 5-baud init, handshake, ident stream, session lifecycle.
 *
 * All operations are non-blocking state machines.
 * The 5-baud sampling is ISR-driven via TIM17.
 */

#include "kl_session.h"
#include "kl_phy.h"
#include "kl_transport.h"
#include "kl_app.h"
#include "psg_prop.h"
#include "main.h"
#include <string.h>

extern TIM_HandleTypeDef htim17;

static KL_Session sess;

/* Forward declarations for BuildCombined (ident data) */
static size_t sess_build_ident(uint8_t *out, size_t maxlen);

/* ================================================================
 * TIM17 configuration (0.1ms tick for 5-baud sampling)
 * ================================================================ */
static void tim17_config(void)
{
    __HAL_TIM_DISABLE(&htim17);
    __HAL_TIM_DISABLE_IT(&htim17, TIM_IT_UPDATE);
    __HAL_TIM_CLEAR_FLAG(&htim17, TIM_FLAG_UPDATE);

    uint32_t timclk = HAL_RCC_GetPCLK2Freq();
    RCC_ClkInitTypeDef clk;
    uint32_t fl;
    HAL_RCC_GetClockConfig(&clk, &fl);
    if (clk.APB2CLKDivider != RCC_HCLK_DIV1) timclk *= 2U;

    const uint32_t target_hz = 10000U;
    uint32_t psc = (timclk + target_hz - 1U) / target_hz - 1U;
    if (psc > 0xFFFFU) psc = 0xFFFFU;

    __HAL_TIM_SET_PRESCALER(&htim17, psc);
    __HAL_TIM_SET_AUTORELOAD(&htim17, 9U);
    __HAL_TIM_SET_COUNTER(&htim17, 0U);

    htim17.Instance->CR1 |= (TIM_CR1_OPM | TIM_CR1_URS);
    htim17.Instance->EGR = TIM_EGR_UG;
    __HAL_TIM_CLEAR_FLAG(&htim17, TIM_FLAG_UPDATE);

    HAL_NVIC_SetPriority(TIM1_TRG_COM_TIM17_IRQn, 6, 0);
    HAL_NVIC_EnableIRQ(TIM1_TRG_COM_TIM17_IRQn);
}

static void tim17_schedule(uint32_t delay_ms)
{
    uint32_t ticks = delay_ms * 10U;
    if (ticks < 1U) ticks = 1U;
    if (ticks > 0x10000U) ticks = 0x10000U;

    __HAL_TIM_DISABLE(&htim17);
    __HAL_TIM_DISABLE_IT(&htim17, TIM_IT_UPDATE);
    __HAL_TIM_CLEAR_FLAG(&htim17, TIM_FLAG_UPDATE);

    __HAL_TIM_SET_AUTORELOAD(&htim17, (uint16_t)(ticks - 1U));
    __HAL_TIM_SET_COUNTER(&htim17, 0U);

    htim17.Instance->EGR = TIM_EGR_UG;
    __HAL_TIM_CLEAR_FLAG(&htim17, TIM_FLAG_UPDATE);

    htim17.Instance->CR1 |= (TIM_CR1_OPM | TIM_CR1_URS);

    __HAL_TIM_ENABLE_IT(&htim17, TIM_IT_UPDATE);
    __HAL_TIM_ENABLE(&htim17);
}

/* ================================================================
 * EXTI / 5-baud arming
 * ================================================================ */
static void sess_rearm_5baud(void)
{
    KL_Phy_DisableUart();

    __HAL_RCC_SYSCFG_CLK_ENABLE();

    /* TX pin idle high (input with pull-up) */
    GPIO_InitTypeDef giTx = {0};
    giTx.Pin   = KL_TX_PIN;
    giTx.Mode  = GPIO_MODE_INPUT;
    giTx.Pull  = GPIO_PULLUP;
    giTx.Speed = GPIO_SPEED_FREQ_LOW;
    HAL_GPIO_Init(KL_GPIO_PORT, &giTx);
    HAL_GPIO_WritePin(KL_GPIO_PORT, KL_TX_PIN, GPIO_PIN_SET);

    /* RX pin: EXTI falling edge */
    GPIO_InitTypeDef giRx = {0};
    giRx.Pin  = KL_RX_PIN;
    giRx.Mode = GPIO_MODE_IT_FALLING;
    giRx.Pull = GPIO_PULLUP;
    HAL_GPIO_Init(KL_GPIO_PORT, &giRx);

    __HAL_GPIO_EXTI_CLEAR_IT(KL_RX_PIN);
    HAL_NVIC_SetPriority(EXTI15_10_IRQn, 5, 0);
    HAL_NVIC_ClearPendingIRQ(EXTI15_10_IRQn);
    HAL_NVIC_EnableIRQ(EXTI15_10_IRQn);
}

static void sess_reset_to_idle(void)
{
    sess.state             = KL_SESS_IDLE;
    sess.five_bit_index    = 0;
    sess.five_byte         = 0;
    sess.five_active       = 0;
    sess.connection_status = 0;
    sess.end_requested     = 0;

    KL_Transport_ResetCounter();

    HAL_TIM_Base_Stop_IT(&htim17);
    __HAL_TIM_DISABLE_IT(&htim17, TIM_IT_UPDATE);

    __HAL_GPIO_EXTI_CLEAR_IT(KL_RX_PIN);
    HAL_NVIC_ClearPendingIRQ(EXTI15_10_IRQn);
    HAL_NVIC_EnableIRQ(EXTI15_10_IRQn);
}

/* ================================================================
 * Init
 * ================================================================ */
void KL_Session_Init(void)
{
    memset(&sess, 0, sizeof(sess));
    KL_Phy_Init();
    tim17_config();
    sess_rearm_5baud();
}

/* ================================================================
 * ISR: GPIO EXTI falling edge on RX pin (start of 5-baud)
 * ================================================================ */
void KL_Session_OnExtiRxFalling(void)
{
    if (sess.state == KL_SESS_IDLE) {
        HAL_NVIC_DisableIRQ(EXTI15_10_IRQn);
        sess.five_active    = 1;
        sess.five_bit_index = 0;
        sess.five_byte      = 0;
        sess.state          = KL_SESS_5BAUD_SAMPLING;
        tim17_schedule(KL_5BAUD_FIRST_MS);
    }
}

/* ================================================================
 * ISR: TIM17 period elapsed (5-baud bit sampling)
 * ================================================================ */
void KL_Session_OnTim17Elapsed(void)
{
    if (!sess.five_active) {
        HAL_TIM_Base_Stop_IT(&htim17);
        return;
    }

    if (sess.state == KL_SESS_5BAUD_SAMPLING) {
        GPIO_PinState pin = HAL_GPIO_ReadPin(KL_GPIO_PORT, KL_RX_PIN);
        uint8_t bit = (pin == GPIO_PIN_SET) ? 1U : 0U;

        sess.five_byte |= (bit << sess.five_bit_index);
        sess.five_bit_index++;

        if (sess.five_bit_index >= 8) {
            sess.five_active = 0;
            HAL_TIM_Base_Stop_IT(&htim17);

            if (sess.five_byte != KL_SLAVE_ADDR) {
                sess_reset_to_idle();
                sess_rearm_5baud();
                return;
            }

            sess.connection_status = 1;
            sess.hs_next_tick = HAL_GetTick() + KL_W1_MS;
            sess.state = KL_SESS_5BAUD_DONE;
        } else {
            tim17_schedule(KL_5BAUD_BIT_MS);
        }
    }
}

/* ================================================================
 * Session kick (extend deadline)
 * ================================================================ */
void KL_Session_Kick(void)
{
    sess.session_deadline = HAL_GetTick() + KL_SESSION_TMO_MS;
}

void KL_Session_RequestEnd(void)
{
    sess.end_requested = 1;
}

uint8_t KL_Session_GetStatus(void)
{
    return sess.connection_status;
}

/* ================================================================
 * Build identification data
 * ================================================================ */
static size_t append_str(const char *s, size_t n, uint8_t *out, size_t w, size_t maxlen)
{
    for (size_t i = 0; i < n && s[i] != '\0' && w < maxlen; i++) {
        out[w++] = (uint8_t)s[i];
    }
    return w;
}

static size_t sess_build_ident(uint8_t *out, size_t maxlen)
{
    size_t w = 0;
    w = append_str(PSG_KUNDENNUMMER_STR, 12, out, w, maxlen);
    w = append_str(PSG_DATENSATZ_STR,    10, out, w, maxlen);
    w = append_str(PSG_SOFTWARE_VER_STR,  10, out, w, maxlen);
    w = append_str(PSG_SOFTWARE_VER2_STR, 10, out, w, maxlen);
    w = append_str(PSG_STEUERGERAET_STR,  10, out, w, maxlen);
    w = append_str(PSG_UNKNOWN_STR,        6, out, w, maxlen);
    return w;
}

/* ================================================================
 * Terminate session and rearm 5-baud
 * ================================================================ */
static void sess_terminate(void)
{
    sess.connection_status = 0;
    sess.session_deadline  = 0;
    sess.end_requested     = 0;

    KL_App_Reset();
    KL_Transport_ResetCounter();
    KL_Transport_TxReset();
    KL_Transport_RxReset();
    KL_Phy_DisableUart();
    sess_rearm_5baud();

    sess.state = KL_SESS_IDLE;
}

/* ================================================================
 * Main session service (called from KLine_Service)
 * ================================================================ */
void KL_Session_Service(void)
{
    uint32_t now = HAL_GetTick();

    switch (sess.state) {

    case KL_SESS_IDLE:
    case KL_SESS_5BAUD_SAMPLING:
        /* Driven by ISR */
        break;

    /* ---- 5-baud done: wait W1 then enter UART mode ---- */
    case KL_SESS_5BAUD_DONE:
        if (kl_tick_diff(now, sess.hs_next_tick) >= 0) {
            KL_Phy_EnableUart9600();
            KL_Transport_Init();
            KL_Phy_RxFlush();

            sess.hs_retry_count = 0;
            sess.hs_next_tick   = now;
            sess.connection_status = 1;
            sess.state = KL_SESS_HS_SYNC;
        }
        break;

    /* ---- Handshake: send sync byte ---- */
    case KL_SESS_HS_SYNC:
        if (kl_tick_diff(now, sess.hs_next_tick) >= 0) {
            if (KL_Transport_SendRawByte(KL_SYNC_BYTE)) {
                sess.state = KL_SESS_HS_SYNC_WAIT;
            }
        }
        break;

    case KL_SESS_HS_SYNC_WAIT:
        if (KL_Transport_RawTxDone()) {
            sess.hs_next_tick = HAL_GetTick() + KL_W2_MS;
            KL_Transport_RawTxReset();
            sess.state = KL_SESS_HS_KW_LSB;
        }
        break;

    /* ---- Handshake: send keyword LSB ---- */
    case KL_SESS_HS_KW_LSB:
        if (kl_tick_diff(now, sess.hs_next_tick) >= 0) {
            if (KL_Transport_SendRawByte(KL_KEYWORD_LSB)) {
                sess.state = KL_SESS_HS_KW_LSB_WAIT;
            }
        }
        break;

    case KL_SESS_HS_KW_LSB_WAIT:
        if (KL_Transport_RawTxDone()) {
            sess.hs_next_tick = HAL_GetTick() + KL_W3_MS;
            KL_Transport_RawTxReset();
            sess.state = KL_SESS_HS_KW_MSB;
        }
        break;

    /* ---- Handshake: send keyword MSB ---- */
    case KL_SESS_HS_KW_MSB:
        if (kl_tick_diff(now, sess.hs_next_tick) >= 0) {
            if (KL_Transport_SendRawByte(KL_KEYWORD_MSB)) {
                sess.state = KL_SESS_HS_KW_MSB_WAIT;
            }
        }
        break;

    case KL_SESS_HS_KW_MSB_WAIT:
        if (KL_Transport_RawTxDone()) {
            KL_Transport_RawTxReset();
            /* Set deadline for master ACK */
            sess.hs_next_tick = HAL_GetTick() + KL_HS_RETRY_MS;
            sess.state = KL_SESS_HS_WAIT_ACK;
        }
        break;

    /* ---- Handshake: wait for master ~KW_MSB ---- */
    case KL_SESS_HS_WAIT_ACK:
    {
        uint8_t rx_byte;
        if (KL_Phy_RxPop(&rx_byte)) {
            if (rx_byte == (uint8_t)~KL_KEYWORD_MSB) {
                /* Handshake succeeded -- prepare identification */
                sess.ident_len    = (uint8_t)sess_build_ident(sess.ident_buf,
                                                              sizeof(sess.ident_buf));
                sess.ident_offset = 0;
                KL_Phy_RxFlush();
                sess.session_deadline = HAL_GetTick() + KL_SESSION_TMO_MS;
                sess.state = KL_SESS_IDENT_SEND;
                break;
            }
            /* Unexpected byte, fall through to retry */
        }
        if (kl_tick_diff(now, sess.hs_next_tick) >= 0) {
            sess.hs_retry_count++;
            if (sess.hs_retry_count >= KL_HANDSHAKE_MAX_RETRIES) {
                sess_terminate();
            } else {
                sess.hs_next_tick = now;
                sess.state = KL_SESS_HS_SYNC;
            }
        }
        break;
    }

    /* ---- Identification: send ASCII chunks ---- */
    case KL_SESS_IDENT_SEND:
    {
        uint8_t remaining = sess.ident_len - sess.ident_offset;
        sess.ident_chunk_len = (remaining > KL_ASCII_CHUNK_SIZE)
                               ? KL_ASCII_CHUNK_SIZE : remaining;

        uint8_t payload[1 + KL_ASCII_CHUNK_SIZE];
        payload[0] = (uint8_t)KL_CMD_AsciiData;
        memcpy(&payload[1], &sess.ident_buf[sess.ident_offset], sess.ident_chunk_len);

        if (KL_Transport_SendPacket(payload, (uint8_t)(1 + sess.ident_chunk_len))) {
            sess.state = KL_SESS_IDENT_WAIT_TX;
        }
        break;
    }

    case KL_SESS_IDENT_WAIT_TX:
        if (kl_tick_diff(now, sess.session_deadline) >= 0) {
            sess_terminate();
            break;
        }
        if (KL_Transport_TxDone()) {
            KL_Transport_TxReset();
            sess.ident_offset += sess.ident_chunk_len;
            if (sess.ident_offset >= sess.ident_len) {
                /* Last chunk sent -- go straight to ready.
                 * No ACK expected after the final chunk. */
                KL_Session_Kick();
                sess.connection_status = 2;
                sess.state = KL_SESS_READY;
            } else {
                /* Intermediate chunk: wait for ACK before next */
                KL_Transport_StartReceive(400);
                sess.state = KL_SESS_IDENT_WAIT_ACK;
            }
        } else if (KL_Transport_TxError()) {
            sess_terminate();
        }
        break;

    case KL_SESS_IDENT_WAIT_ACK:
        if (kl_tick_diff(now, sess.session_deadline) >= 0) {
            sess_terminate();
            break;
        }
        if (KL_Transport_RxDone()) {
            const KL_Packet *pkt = KL_Transport_GetRxPacket();
            KL_Transport_RxReset();
            if (pkt->command == (uint8_t)KL_CMD_ACK) {
                sess.state = KL_SESS_IDENT_SEND;
            } else {
                sess_terminate();
            }
        } else if (KL_Transport_RxError()) {
            sess_terminate();
        }
        break;

    case KL_SESS_IDENT_FINAL_WAIT:
        /* No longer used, kept for enum completeness */
        if (KL_Transport_TxDone()) {
            KL_Transport_TxReset();
            KL_Session_Kick();
            sess.connection_status = 2;
            sess.state = KL_SESS_READY;
        } else if (KL_Transport_TxError()) {
            sess_terminate();
        }
        break;

    /* ---- Active session: ready to receive commands ---- */
    case KL_SESS_READY:
        if (kl_tick_diff(now, sess.session_deadline) >= 0) {
            sess_terminate();
            break;
        }
        /* Use the session deadline as the RX timeout so we don't
         * artificially cut short a receive that started late.
         * Compute remaining time until session deadline. */
        {
            int32_t remaining = (int32_t)(sess.session_deadline - now);
            if (remaining <= 0) { sess_terminate(); break; }
            if (KL_Transport_StartReceive((uint32_t)remaining)) {
                sess.state = KL_SESS_CMD_RX;
            }
            /* If StartReceive fails (rx not idle), stay in READY
             * and retry next cycle rather than advancing to CMD_RX
             * with no active receive. */
        }
        break;

    case KL_SESS_CMD_RX:
        if (kl_tick_diff(now, sess.session_deadline) >= 0) {
            KL_Transport_RxReset();
            sess_terminate();
            break;
        }
        if (KL_Transport_RxDone()) {
            KL_Session_Kick();
            sess.state = KL_SESS_CMD_DISPATCH;
        } else if (KL_Transport_RxError()) {
            KL_Transport_RxReset();
            /* Only send NAK if we actually received partial data
             * (i.e., at least the length byte was consumed).
             * A plain timeout with no data is just "no packet yet"
             * and should silently retry. */
            if (KL_Transport_RxHadData()) {
                KL_Transport_SendNak();
                sess.state = KL_SESS_CMD_NAK_WAIT;
            } else {
                sess.state = KL_SESS_READY;
            }
        }
        break;

    case KL_SESS_CMD_NAK_WAIT:
        if (kl_tick_diff(now, sess.session_deadline) >= 0) {
            KL_Transport_TxReset();
            sess_terminate();
            break;
        }
        if (KL_Transport_TxDone() || KL_Transport_TxError()) {
            KL_Transport_TxReset();
            sess.state = KL_SESS_READY;
        }
        break;

    case KL_SESS_CMD_DISPATCH:
    {
        const KL_Packet *pkt = KL_Transport_GetRxPacket();
        KL_Transport_RxReset();
        KL_App_Dispatch(pkt);
        sess.state = KL_SESS_CMD_RESPONSE;
        break;
    }

    case KL_SESS_CMD_RESPONSE:
        if (kl_tick_diff(now, sess.session_deadline) >= 0) {
            sess_terminate();
            break;
        }
        if (KL_App_Done()) {
            if (sess.end_requested) {
                sess_terminate();
            } else {
                KL_Session_Kick();
                sess.state = KL_SESS_READY;
            }
        }
        break;

    case KL_SESS_TERMINATE:
        sess_terminate();
        break;
    }
}
