/*
 * kline_fsm.c
 *
 * Full non-blocking KWP1281 state machine.
 *
 * Timing model
 * ------------
 * Every state that needs a delay stores a deadline:
 *     fsm.deadline = HAL_GetTick() + N_ms;
 * and on the next ST_xxx entry just checks:
 *     if ((int32_t)(HAL_GetTick() - fsm.deadline) < 0) return;
 *
 * This means KLine_FSM_Run() returns almost immediately on every call
 * and the main loop stays responsive for HC-06 processing.
 *
 * Zero HAL_Delay calls anywhere in this file.
 */

#include "kline_fsm.h"
#include "kline_ring.h"
#include "main.h"         /* huart1, KLINE_GPIO_PORT, KLINE_PIN             */
#include "hc_06.h"        /* BT_OnPSG5CommEstablished, BT_SendCommStatus    */

#include <string.h>
#include <stdint.h>

/* ------------------------------------------------------------------ */
/*  Internal timing constants (ms)                                      */
/* ------------------------------------------------------------------ */

#define BITBANG_BIT_MS          200u   /* 5 baud = 200 ms/bit                */
#define KWP_INTERBYTE_TX_MS     5u     /* between bytes we send              */
#define KWP_BYTE_TIMEOUT_MS     1000u  /* max wait for a byte from ECU       */
#define KWP_KW2_COMP_DELAY_MS   5u    /* delay before sending ~KW2          */
#define KWP_KEEPALIVE_PERIOD_MS 500u   /* how often we must send keep-alive  */

/* ------------------------------------------------------------------ */
/*  Low-level TX helpers (synchronous – tiny at 9600 baud, safe)       */
/* ------------------------------------------------------------------ */

static void tx_byte(uint8_t b)
{
    HAL_UART_Transmit(&huart1, &b, 1, 10);
}

void KLine_FSM_TxByte(uint8_t b)
{
    tx_byte(b);
}

/* ------------------------------------------------------------------ */
/*  Packet parsing helpers                                              */
/* ------------------------------------------------------------------ */

#define MAX_RECV_PACKETS  16
#define MAX_PACKET_RAW    20

/* Scratch packet being assembled right now */
static ParsedPacket  cur_pkt;
static uint8_t       cur_pkt_body_idx;   /* how many body bytes received so far */

/* Completed packet list for the current ReceivePackets burst */
static ParsedPacket  pkt_list[MAX_RECV_PACKETS];
static int           pkt_count;

static uint8_t       pkt_counter_val;
static uint8_t       pkt_counter_init;

static void classify_packet(ParsedPacket *p)
{
    p->title = p->raw[2];
    p->isAckNak = 0;

    switch (p->raw[2]) {
        case PACKET_CMD_ACK:
            p->type = PACKET_TYPE_ACK;
            p->isAckNak = 1;
            break;
        case PACKET_CMD_NAK:
            p->type = PACKET_TYPE_NAK;
            p->isAckNak = 1;
            break;
        case PACKET_CMD_AsciiData:
            p->type = (p->raw[3] == 0x00) ? PACKET_TYPE_CODING_WSC
                                           : PACKET_TYPE_ASCII_DATA;
            break;
        case PACKET_CMD_ReadEepromResponse:
            p->type = PACKET_TYPE_READ_EEPROM_RESPONSE;
            break;
        case PACKET_CMD_ReadRomEepromResponse:
            p->type = PACKET_TYPE_READ_ROM_EEPROM_RESPONSE;
            break;
        default:
            p->type = PACKET_TYPE_UNKNOWN;
            break;
    }
}

/* ------------------------------------------------------------------ */
/*  Outgoing packet scratch buffer                                      */
/* ------------------------------------------------------------------ */

#define MAX_SEND_PACKET 17

static uint8_t  send_buf[MAX_SEND_PACKET];
static uint8_t  send_len;          /* total bytes in send_buf (excl. end byte) */
static uint8_t  send_idx;          /* index of next byte to transmit           */

//static uint8_t  tx_pkt_counter = 0;

static void build_packet(const uint8_t *payload, uint8_t payload_len)
{
    send_len = payload_len + 2;            /* length byte + counter byte + payload */
    send_buf[0] = send_len;
    send_buf[1] = pkt_counter_val++;
    for (uint8_t i = 0; i < payload_len; i++)
        send_buf[2 + i] = payload[i];
    //send_buf[2 + payload_len] = PACKET_END_EXPECTED;
    send_idx = 0;
}

/* ------------------------------------------------------------------ */
/*  FSM context                                                         */
/* ------------------------------------------------------------------ */

typedef enum {
    /* What to do after the generic send-packet sub-FSM finishes */
    AFTER_SEND_IDLE,
    AFTER_SEND_RECV_ECUINFO,
    AFTER_SEND_RECV_IDENT,
    AFTER_SEND_RECV_KA,
    AFTER_SEND_RECV_FAULTCODES,
} AfterSend;

typedef enum {
    /* What to do after the generic recv-packets sub-FSM finishes */
    AFTER_RECV_ECUINFO,
    AFTER_RECV_IDENT,
    AFTER_RECV_KA,
    AFTER_RECV_FAULTCODES,
} AfterRecv;

typedef struct {
    KLine_FSMState  state;
    KLine_Status    status;
    uint32_t        deadline;

    /* 5-baud bit-bang */
    uint8_t         bb_addr;
    uint8_t         bb_bit;        /* current bit index 0-7 */

    /* WakeUp */
    uint8_t         kw1;

    /* Generic TX sub-machine */
    AfterSend       after_send;
    uint8_t         last_sent_byte;  /* for ACK check */

    /* Generic RX sub-machine */
    AfterRecv       after_recv;

    /* ECU info */
    ControllerInfo  ecu_info;

    /* Fault codes */
    KLine_FaultCode fault_codes[KLINE_MAX_FAULT_CODES];
    int             fault_count;

    /* Keep-alive */
    uint32_t        ka_due;
    uint8_t         ka_suspended;  /* non-zero → KA paused while an op runs */

    /* IdentifyEcu phase counter (how many ident packets received so far) */
    uint8_t         ident_phase;

} KLine_FSM;

static KLine_FSM fsm;

/* ------------------------------------------------------------------ */
/*  Forward declarations for sub-machine entry points                  */
/* ------------------------------------------------------------------ */

static void enter_send_packet(const uint8_t *payload, uint8_t len, AfterSend after);
static void enter_recv_packets(AfterRecv after);

/* ------------------------------------------------------------------ */
/*  Byte-level receive helpers                                          */
/* ------------------------------------------------------------------ */

/*
 * Try to read a byte with complement-ACK from the ECU.
 * Returns 1 and stores byte in *out if available, 0 if still waiting.
 * Handles timeout: transitions to ST_ERROR on timeout.
 */
static uint8_t try_read_and_ack(uint8_t *out)
{
    if (!KLine_Ring_Available()) {
        if ((int32_t)(HAL_GetTick() - fsm.deadline) >= 0) {
            fsm.state  = ST_ERROR;
            fsm.status = KLINE_STATUS_ERROR;
        }
        return 0;
    }
    *out = KLine_Ring_Pop();
    tx_byte((uint8_t)~(*out));        /* send complement ACK */


    uint32_t echo_deadline = HAL_GetTick() + 5u;  /* ~1ms at 9600, 5ms generous */
    while (!KLine_Ring_Available()) {
        if ((int32_t)(HAL_GetTick() - echo_deadline) >= 0) break;
    }
    if (KLine_Ring_Available()) KLine_Ring_Pop();

    fsm.deadline = HAL_GetTick() + KWP_BYTE_TIMEOUT_MS;
    return 1;
}

static uint8_t try_read_raw(uint8_t *out)
{
    if (!KLine_Ring_Available()) {
        if ((int32_t)(HAL_GetTick() - fsm.deadline) >= 0) {
            fsm.state  = ST_ERROR;
            fsm.status = KLINE_STATUS_ERROR;
        }
        return 0;
    }
    *out = KLine_Ring_Pop();
    fsm.deadline = HAL_GetTick() + KWP_BYTE_TIMEOUT_MS;
    return 1;
}

/* ------------------------------------------------------------------ */
/*  ECU info parser                                                     */
/* ------------------------------------------------------------------ */

static void parse_ecu_info(void)
{
    char combined[128] = {0};
    for (int i = 0; i < pkt_count; i++) {
        ParsedPacket *p = &pkt_list[i];
        if (p->type == PACKET_TYPE_ASCII_DATA && p->length > 4) {
            size_t len = p->length - 4;
            strncat(combined, (char*)(p->raw + 3), len);
        }
    }
    memset(&fsm.ecu_info, 0, sizeof(ControllerInfo));
    strncpy(fsm.ecu_info.client_ident, combined,      12);
    strncpy(fsm.ecu_info.unk_ident1,   combined + 12, 10);
    strncpy(fsm.ecu_info.soft_info,    combined + 22, 10);
    if (strlen(combined) > 40)
        strncpy(fsm.ecu_info.unk_ident2, combined + 32, 10);
    if (strlen(combined) > 50)
        strncpy(fsm.ecu_info.unk_ident3, combined + 42, 10);
}

/* ------------------------------------------------------------------ */
/*  Fault code parser                                                   */
/* ------------------------------------------------------------------ */

static void parse_fault_codes(void)
{
    uint8_t fc_buf[64];
    int     fc_len = 0;

    for (int i = 0; i < pkt_count; i++) {
        if (pkt_list[i].isAckNak) continue;
        if (pkt_list[i].title != PACKET_CMD_FaultCodesResponse) continue;
        int body = (int)pkt_list[i].length - 4;
        if (body <= 0) continue;
        if (fc_len + body > (int)sizeof(fc_buf)) break;
        memcpy(&fc_buf[fc_len], &pkt_list[i].raw[3], (size_t)body);
        fc_len += body;
    }

    fsm.fault_count = 0;
    int off = 0;
    while (off + 3 <= fc_len && fsm.fault_count < KLINE_MAX_FAULT_CODES) {
        if (fc_buf[off] != 0x00) {
            fsm.fault_codes[fsm.fault_count].dtc    = fc_buf[off + 0];
            fsm.fault_codes[fsm.fault_count].status = fc_buf[off + 1];
            fsm.fault_codes[fsm.fault_count].extra  = fc_buf[off + 2];
            fsm.fault_count++;
        }
        off += 8;
    }
}

/* ------------------------------------------------------------------ */
/*  Sub-machine entry helpers                                           */
/* ------------------------------------------------------------------ */

static void enter_send_packet(const uint8_t *payload, uint8_t len, AfterSend after)
{
    build_packet(payload, len);
    fsm.after_send = after;
    fsm.state      = ST_SENDPKT_BYTE;
    fsm.deadline   = HAL_GetTick() + KWP_BYTE_TIMEOUT_MS;
}

static void enter_recv_packets(AfterRecv after)
{
    pkt_count          = 0;
    pkt_counter_init   = 0;
    fsm.after_recv     = after;
    /* Start assembling the first packet */
    memset(&cur_pkt, 0, sizeof(cur_pkt));
    cur_pkt_body_idx   = 0;
    fsm.state          = ST_RECVPKT_LENGTH;
    fsm.deadline       = HAL_GetTick() + KWP_BYTE_TIMEOUT_MS;
}

/* ------------------------------------------------------------------ */
/*  Main FSM tick                                                       */
/* ------------------------------------------------------------------ */

void KLine_FSM_Run(void)
{
    switch (fsm.state)
    {
    /* ====================================================== */
    case ST_IDLE:
        /* Nothing to do — wait for KLine_FSM_RequestConnect() */
        break;

    /* ====================================================== */
    /*  5-baud bit-bang init                                   */
    /* ====================================================== */

    case ST_BITBANG_START:
    {
        /* De-init UART, take over TX pin as GPIO */
        HAL_UART_DeInit(&huart1);

        GPIO_InitTypeDef g = {0};
        g.Pin   = KLINE_PIN;
        g.Mode  = GPIO_MODE_OUTPUT_PP;
        g.Pull  = GPIO_NOPULL;
        g.Speed = GPIO_SPEED_FREQ_LOW;
        HAL_GPIO_Init(KLINE_GPIO_PORT, &g);

        /* Drive start bit (low) */
        HAL_GPIO_WritePin(KLINE_GPIO_PORT, KLINE_PIN, GPIO_PIN_RESET);
        fsm.bb_bit  = 0;
        fsm.deadline = HAL_GetTick() + BITBANG_BIT_MS;
        fsm.state   = ST_BITBANG_STARTBIT;
        break;
    }

    case ST_BITBANG_STARTBIT:
        if ((int32_t)(HAL_GetTick() - fsm.deadline) < 0) return;
        /* Fall into first data bit */
        fsm.state = ST_BITBANG_DATABIT;
        /* intentional fall-through */
        /* fall through */

    case ST_BITBANG_DATABIT:
    {
        if (fsm.bb_bit > 0) {
            /* We get here after a bit-delay has elapsed */
            if ((int32_t)(HAL_GetTick() - fsm.deadline) < 0) return;
        }
        if (fsm.bb_bit == 8) {
            /* All data bits sent — send stop bit */
            HAL_GPIO_WritePin(KLINE_GPIO_PORT, KLINE_PIN, GPIO_PIN_SET);
            fsm.deadline = HAL_GetTick() + BITBANG_BIT_MS;
            fsm.state    = ST_BITBANG_STOPBIT;
            break;
        }
        GPIO_PinState level = (fsm.bb_addr & (1u << fsm.bb_bit))
                              ? GPIO_PIN_SET : GPIO_PIN_RESET;
        HAL_GPIO_WritePin(KLINE_GPIO_PORT, KLINE_PIN, level);
        fsm.bb_bit++;
        fsm.deadline = HAL_GetTick() + BITBANG_BIT_MS;
        break;
    }

    case ST_BITBANG_STOPBIT:
        if ((int32_t)(HAL_GetTick() - fsm.deadline) < 0) return;
        fsm.state = ST_BITBANG_DONE;
        break;

    case ST_BITBANG_DONE:
    {
        /* Restore UART */
        extern void MX_USART1_UART_Init(void);   /* generated by CubeMX */
        MX_USART1_UART_Init();
        KLine_Ring_Init();          /* flushes ring and arms RX IT */

        fsm.deadline = HAL_GetTick() + KWP_BYTE_TIMEOUT_MS;
        fsm.state    = ST_WAIT_SYNC;
        break;
    }

    /* ====================================================== */
    /*  WakeUp handshake                                       */
    /* ====================================================== */

    case ST_WAIT_SYNC:
    {
        uint8_t b;
        if (!try_read_raw(&b)) return;
        if (b != 0x55) {
            fsm.state  = ST_ERROR;
            fsm.status = KLINE_STATUS_ERROR;
            return;
        }
        fsm.state    = ST_READ_KW1;
        fsm.deadline = HAL_GetTick() + KWP_BYTE_TIMEOUT_MS;
        break;
    }

    case ST_READ_KW1:
    {
        uint8_t b;
        if (!try_read_raw(&b)) return;
        //tx_byte((uint8_t)~b);       /* NO ACK KW1 */
        fsm.kw1      = b;
        fsm.state    = ST_READ_KW2;
        fsm.deadline = HAL_GetTick() + KWP_BYTE_TIMEOUT_MS;
        break;
    }

    case ST_READ_KW2:
    {
        uint8_t b;
        if (!try_read_raw(&b)) return;
        /* Store KW2, schedule complement send after inter-byte delay */
        send_buf[0]  = (uint8_t)~b;   /* reuse send_buf[0] as a scratch */
        fsm.deadline = HAL_GetTick() + KWP_KW2_COMP_DELAY_MS;
        fsm.state    = ST_SEND_KW2_COMPLEMENT;
        break;
    }

    case ST_SEND_KW2_COMPLEMENT:
        if ((int32_t)(HAL_GetTick() - fsm.deadline) < 0) return;
        tx_byte(send_buf[0]);   /* send ~KW2 */

        //uint8_t b;
        //if (!try_read_raw(&b)) return; //should return answer to kw2
        while (!KLine_Ring_Available()) {}
        KLine_Ring_Pop();       /* discard loopback echo */

        /* Now receive the ECU info packet burst */
        enter_recv_packets(AFTER_RECV_ECUINFO);
        break;

    /* ====================================================== */
    /*  Generic send-packet sub-machine                        */
    /* ====================================================== */

    case ST_SENDPKT_BYTE:
    {
        if (send_idx >= send_len) {
            /* All bytes sent — send end byte (no ACK) */
            tx_byte(PACKET_END_EXPECTED);
            while (!KLine_Ring_Available()) {}
            KLine_Ring_Pop();       /* discard loopback echo */
            /* Transition based on what comes after this send */
            switch (fsm.after_send) {
                case AFTER_SEND_RECV_ECUINFO:
                    enter_recv_packets(AFTER_RECV_ECUINFO);   break;
                case AFTER_SEND_RECV_IDENT:
                    enter_recv_packets(AFTER_RECV_IDENT);     break;
                case AFTER_SEND_RECV_KA:
                    enter_recv_packets(AFTER_RECV_KA);        break;
                case AFTER_SEND_RECV_FAULTCODES:
                    enter_recv_packets(AFTER_RECV_FAULTCODES);break;
                default:
                    fsm.state = ST_KA_WAIT;                   break;
            }
            break;
        }
        /* Send next byte */
        fsm.last_sent_byte = send_buf[send_idx++];
        tx_byte(fsm.last_sent_byte);
        while (!KLine_Ring_Available()) {}
        KLine_Ring_Pop();       /* discard loopback echo */

        fsm.deadline = HAL_GetTick() + KWP_BYTE_TIMEOUT_MS;
        fsm.state    = ST_SENDPKT_WAIT_ACK;
        break;
    }

    case ST_SENDPKT_WAIT_ACK:
    {
        /* Wait for ECU to echo ~byte as ACK */
        if (!KLine_Ring_Available()) {
            if ((int32_t)(HAL_GetTick() - fsm.deadline) >= 0) {
                fsm.state  = ST_ERROR;
                fsm.status = KLINE_STATUS_ERROR;
            }
            return;
        }
        uint8_t ack = KLine_Ring_Pop();
        (void)ack;  /* Could check ack == ~last_sent_byte here */
        /* Inter-byte delay before next byte */
        fsm.deadline = HAL_GetTick() + KWP_INTERBYTE_TX_MS;
        fsm.state    = ST_SENDPKT_NEXT_BYTE;
        break;
    }

    case ST_SENDPKT_NEXT_BYTE:
        if ((int32_t)(HAL_GetTick() - fsm.deadline) < 0) return;
        fsm.state = ST_SENDPKT_BYTE;
        break;

    /* ====================================================== */
    /*  Generic receive-packets sub-machine                    */
    /* ====================================================== */

    case ST_RECVPKT_LENGTH:
    {
        //if ((int32_t)(HAL_GetTick() - (fsm.deadline - KWP_BYTE_TIMEOUT_MS + 25)) < 0) return;

        uint8_t b;
        if (!try_read_and_ack(&b)) return;

    	if(send_len == 3){
    		send_len++;
    	}
        memset(&cur_pkt, 0, sizeof(cur_pkt));
        cur_pkt_body_idx = 0;
        cur_pkt.raw[0]   = b;   /* raw[0] = length byte */
        fsm.state        = ST_RECVPKT_COUNTER;
        break;
    }

    case ST_RECVPKT_COUNTER:
    {
        uint8_t b;
        if (!try_read_and_ack(&b)) return;
        if (!pkt_counter_init) {
            pkt_counter_val  = b;
            pkt_counter_init = 1;
        }
        pkt_counter_val++;
        cur_pkt.raw[1] = b;
        fsm.state      = ST_RECVPKT_COMMAND;
        break;
    }

    case ST_RECVPKT_COMMAND:
    {
        uint8_t b;
        if (!try_read_and_ack(&b)) return;
        cur_pkt.raw[2] = b;
        cur_pkt_body_idx = 3;
        /* body length = packetLength - 3 (len, counter, command already read) */
        /* The end byte (0x03) is read separately in ST_RECVPKT_END            */
        if (cur_pkt.raw[0] <= 3) {
            /* No body bytes — go straight to end */
            fsm.state = ST_RECVPKT_END;
        } else {
            fsm.state = ST_RECVPKT_BODY;
        }
        if(b == PACKET_CMD_ACK || b == PACKET_CMD_NAK){
        	cur_pkt.isAckNak = 1;
        }else{
        	cur_pkt.isAckNak = 0;
        }
        break;
    }

    case ST_RECVPKT_BODY:
    {
        uint8_t b;
        if (!try_read_and_ack(&b)) return;
        if (cur_pkt_body_idx < MAX_PACKET_RAW)
            cur_pkt.raw[cur_pkt_body_idx] = b;
        cur_pkt_body_idx++;
        /* body bytes = raw[0] - 3, so we've finished when we have raw[0] bytes total */
        if (cur_pkt_body_idx >= cur_pkt.raw[0]) {
            fsm.state = ST_RECVPKT_END;
        }
        break;
    }

    case ST_RECVPKT_END:
    {
        uint8_t b;
        if (!try_read_raw(&b)) return;   /* end byte – no ACK */
        cur_pkt.raw[cur_pkt_body_idx] = b;
        cur_pkt.length = cur_pkt_body_idx + 1;
        classify_packet(&cur_pkt);

        if (pkt_count < MAX_RECV_PACKETS)
            pkt_list[pkt_count++] = cur_pkt;

        if (cur_pkt.isAckNak) {
            /* This burst is complete — dispatch */
            switch (fsm.after_recv) {

                case AFTER_RECV_ECUINFO:
                    parse_ecu_info();
                    /* Now do IdentifyEcu: send ReadIdent, receive ident packets */
                    fsm.ident_phase = 0;
                    fsm.state       = ST_IDENT_KEEPALIVE_SEND;
                    break;

                case AFTER_RECV_IDENT:
                    /* More ident packets may follow — check if we need another round */
                    fsm.ident_phase++;
                    if (fsm.ident_phase < 3) {
                        /* Send another ReadIdent ACK and receive next burst */
                        fsm.state = ST_IDENT_KEEPALIVE_SEND;
                    } else {
                        /* Done identifying — enter keep-alive + fault code read */
                        uint8_t cmd = (uint8_t)PACKET_CMD_FaultCodesRead;
                        enter_send_packet(&cmd, 1, AFTER_SEND_RECV_FAULTCODES);
                    }
                    break;

                case AFTER_RECV_KA:
                    /* Keep-alive round-trip complete */
                    if (pkt_list[pkt_count-1].type != PACKET_TYPE_ACK) {
                        fsm.state  = ST_ERROR;
                        fsm.status = KLINE_STATUS_ERROR;
                        break;
                    }
                    KLine_OnKeepAliveTick();
                    fsm.ka_due = HAL_GetTick() + KWP_KEEPALIVE_PERIOD_MS;
                    fsm.state  = ST_KA_WAIT;
                    break;

                case AFTER_RECV_FAULTCODES:
                    parse_fault_codes();
                    /* Session fully established */
                    fsm.status = KLINE_STATUS_CONNECTED;
                    KLine_OnConnected();
                    fsm.ka_due = HAL_GetTick() + KWP_KEEPALIVE_PERIOD_MS;
                    fsm.state  = ST_KA_WAIT;
                    break;
            }
        } else {
            /* More packets in this burst — send ACK then read next */
            fsm.state = ST_RECVPKT_SEND_ACK;
        }
        break;
    }

    case ST_RECVPKT_SEND_ACK:
    {
        /* Send ACK packet (len=3, counter, 0x09) then read next packet */
        uint8_t ack_payload = (uint8_t)PACKET_CMD_ACK;
        enter_send_packet(&ack_payload, 1, AFTER_SEND_IDLE);

        /* After sending the ACK packet we continue receiving in the same burst */
        fsm.after_send = AFTER_SEND_IDLE;  /* send sub-machine won't auto-recv */

        /* Prepare for next packet in burst */
        memset(&cur_pkt, 0, sizeof(cur_pkt));
        cur_pkt_body_idx = 0;

        /* Override the send sub-machine's post-send state to loop back to recv */
        /* We do this by saving after_recv and re-entering after send completes  */
        /* Simplest: finish the send inline here before entering recv again.     */
        /* Actually: enter_send_packet already set state = ST_SENDPKT_BYTE,     */
        /* we need it to go to ST_RECVPKT_LENGTH after. We tag after_send.      */
        switch (fsm.after_recv) {
            case AFTER_RECV_ECUINFO:
                fsm.after_send = AFTER_SEND_RECV_ECUINFO; break;
            case AFTER_RECV_IDENT:
                fsm.after_send = AFTER_SEND_RECV_IDENT;   break;
            case AFTER_RECV_KA:
                fsm.after_send = AFTER_SEND_RECV_KA;      break;
            case AFTER_RECV_FAULTCODES:
                fsm.after_send = AFTER_SEND_RECV_FAULTCODES; break;
        }
        break;
    }

    /* ====================================================== */
    /*  IdentifyEcu sub-sequence                               */
    /* ====================================================== */

    case ST_IDENT_KEEPALIVE_SEND:
    {
        /* Send ACK packet as keep-alive / handshake before ReadIdent */
        uint8_t ack = (uint8_t)PACKET_CMD_ACK;
        enter_send_packet(&ack, 1, AFTER_SEND_RECV_IDENT);
        /* After sending ACK we expect a packet burst back */
        /* The after_recv is set inside enter_recv_packets in AFTER_SEND_RECV_IDENT path */
        /* Override: we actually want AFTER_RECV_IDENT for the receive burst */
        fsm.after_send = AFTER_SEND_RECV_IDENT;
        break;
    }

    /* ====================================================== */
    /*  Keep-alive steady state                                */
    /* ====================================================== */

    case ST_KA_WAIT:
        if (fsm.ka_suspended) return;   /* operations running — don't send KA */
        if ((int32_t)(HAL_GetTick() - fsm.ka_due) < 0) return;
        /* Time to send a keep-alive */
        {
            uint8_t ack = (uint8_t)PACKET_CMD_ACK;
            enter_send_packet(&ack, 1, AFTER_SEND_RECV_KA);
        }
        break;

    /* ====================================================== */
    /*  Error                                                  */
    /* ====================================================== */

    case ST_ERROR:
        fsm.status = KLINE_STATUS_ERROR;
        KLine_OnDisconnected();
        /* Disarm UART and flush ring */
        HAL_UART_AbortReceive(&huart1);
        KLine_Ring_Flush();
        fsm.state = ST_IDLE;
        break;

    default:
        break;
    }
}

/* ------------------------------------------------------------------ */
/*  Public API                                                          */
/* ------------------------------------------------------------------ */

void KLine_FSM_Init(void)
{
    memset(&fsm, 0, sizeof(fsm));
    fsm.state  = ST_IDLE;
    fsm.status = KLINE_STATUS_IDLE;
}

void KLine_FSM_RequestConnect(uint8_t ecuAddress)
{
    if (fsm.state != ST_IDLE) return;
    memset(&fsm, 0, sizeof(fsm));
    fsm.bb_addr = ecuAddress;
    fsm.status  = KLINE_STATUS_CONNECTING;
    fsm.state   = ST_BITBANG_START;
}

KLine_Status KLine_FSM_GetStatus(void)
{
    return fsm.status;
}

uint8_t KLine_FSM_IsConnected(void)
{
    return fsm.status == KLINE_STATUS_CONNECTED;
}

const ControllerInfo* KLine_FSM_GetEcuInfo(void)
{
    return &fsm.ecu_info;
}

const KLine_FaultCode* KLine_FSM_GetFaultCodes(int *out_count)
{
    if (out_count) *out_count = fsm.fault_count;
    return fsm.fault_codes;
}

/* ------------------------------------------------------------------ */
/*  Operations support API                                              */
/* ------------------------------------------------------------------ */

uint8_t KLine_FSM_GetNextCounter(void)
{
    return pkt_counter_val++;
}

void KLine_FSM_SuspendKA(uint8_t suspend)
{
    fsm.ka_suspended = suspend;
    if (!suspend) {
        /* Resume: push deadline forward so we don't immediately timeout */
        fsm.ka_due = HAL_GetTick() + KWP_KEEPALIVE_PERIOD_MS;
    }
}

void KLine_FSM_ResetKATimer(void)
{
    fsm.ka_due = HAL_GetTick() + KWP_KEEPALIVE_PERIOD_MS;
}

/* ------------------------------------------------------------------ */
/*  Weak default callbacks — override in main.c                        */
/* ------------------------------------------------------------------ */

__attribute__((weak)) void KLine_OnConnected(void)    {}
__attribute__((weak)) void KLine_OnDisconnected(void) {}
__attribute__((weak)) void KLine_OnKeepAliveTick(void){}
