/**
 * @file config.c
 * @brief Bare-metal final AT configuration module implementation.
 */

#include "config.h"

#include "flash_param.h"
#include "../Core/Inc/usart.h"

#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define CONFIG_TX_BUFFER_SIZE   512u
#define CONFIG_CMD_MAX_LEN      160u

#define CONFIG_UART_HANDLE      huart1

typedef struct {
    uint32_t magic;
    uint16_t version;
    uint16_t reserved;
    uint8_t mac[6];
    uint8_t dhcp_enable;
    uint8_t reserved2;
    uint8_t ip[4];
    uint8_t mask[4];
    uint8_t gw[4];
    uint16_t server_port;
    uint16_t reserved3;
    uint8_t remote_ip[4];
    uint16_t remote_port;
    uint16_t reconnect_interval;
    uint32_t uart2_baudrate;
    uint32_t uart3_baudrate;
    uint8_t uart2_databits;
    uint8_t uart2_stopbits;
    uint8_t uart2_parity;
    uint8_t uart3_databits;
    uint8_t uart3_stopbits;
    uint8_t uart3_parity;
    uint16_t reserved4;
    uint32_t crc;
} legacy_device_config_v2_t;

static device_config_t g_config;
static volatile bool g_reset_requested;
static uint8_t g_uart_cmd_buffer[CONFIG_CMD_MAX_LEN];
static uint16_t g_uart_cmd_len;
static bool g_uart_rx_seen_cr;
static char g_pending_cmd_buffer[CONFIG_CMD_MAX_LEN];
static volatile uint16_t g_pending_cmd_len;
static volatile bool g_pending_cmd_ready;
static char g_at_response_buffer[CONFIG_TX_BUFFER_SIZE];
static char g_cmd_parse_buffer[CONFIG_CMD_MAX_LEN];
static char g_cmd_work_buffer[CONFIG_CMD_MAX_LEN];

static uint32_t config_calc_crc(const device_config_t *cfg)
{
    return flash_param_crc32(cfg, offsetof(device_config_t, crc));
}

static const char *skip_whitespace(const char *str)
{
    while (*str == ' ' || *str == '\t') {
        ++str;
    }
    return str;
}

static void trim_trailing(char *str)
{
    int len = (int)strlen(str);

    while (len > 0 && (str[len - 1] == ' ' || str[len - 1] == '\t' || str[len - 1] == '\r' || str[len - 1] == '\n')) {
        str[--len] = '\0';
    }
}

static bool equals_ignore_case(const char *a, const char *b)
{
    while (*a != '\0' && *b != '\0') {
        char c1 = *a++;
        char c2 = *b++;

        if (c1 >= 'a' && c1 <= 'z') {
            c1 -= 32;
        }
        if (c2 >= 'a' && c2 <= 'z') {
            c2 -= 32;
        }

        if (c1 != c2) {
            return false;
        }
    }

    return (*a == '\0' && *b == '\0');
}

static int prefix_equals_ignore_case(const char *str, const char *prefix)
{
    while (*prefix != '\0') {
        char c1 = *str++;
        char c2 = *prefix++;

        if (c1 >= 'a' && c1 <= 'z') {
            c1 -= 32;
        }
        if (c2 >= 'a' && c2 <= 'z') {
            c2 -= 32;
        }
        if (c1 != c2) {
            return 0;
        }
    }
    return 1;
}

static int parse_u32_value(const char *value, uint32_t min_value, uint32_t max_value, uint32_t *parsed_value)
{
    char *endptr;
    unsigned long parsed;

    if (value == NULL || parsed_value == NULL) {
        return -1;
    }

    parsed = strtoul(value, &endptr, 10);
    if (endptr == value || *skip_whitespace(endptr) != '\0') {
        return -1;
    }
    if (parsed < min_value || parsed > max_value) {
        return -1;
    }

    *parsed_value = (uint32_t)parsed;
    return 0;
}

static int parse_link_uart(const char *value, uint8_t *uart)
{
    if (equals_ignore_case(value, "U0")) {
        *uart = LINK_UART_U0;
        return 0;
    }
    if (equals_ignore_case(value, "U1")) {
        *uart = LINK_UART_U1;
        return 0;
    }
    return -1;
}

static const char *link_uart_to_str(uint8_t uart)
{
    return (uart == LINK_UART_U1) ? "U1" : "U0";
}

static const char *link_index_to_name(uint32_t index)
{
    switch (index) {
    case CONFIG_LINK_S1:
        return "S1";
    case CONFIG_LINK_S2:
        return "S2";
    case CONFIG_LINK_C1:
        return "C1";
    case CONFIG_LINK_C2:
        return "C2";
    default:
        return "?";
    }
}

static int parse_link_name(const char *value, uint32_t *index)
{
    if (value == NULL || index == NULL) {
        return -1;
    }

    if (equals_ignore_case(value, "S1")) {
        *index = CONFIG_LINK_S1;
        return 0;
    }
    if (equals_ignore_case(value, "S2")) {
        *index = CONFIG_LINK_S2;
        return 0;
    }
    if (equals_ignore_case(value, "C1")) {
        *index = CONFIG_LINK_C1;
        return 0;
    }
    if (equals_ignore_case(value, "C2")) {
        *index = CONFIG_LINK_C2;
        return 0;
    }

    return -1;
}

static bool parse_command_with_value(const char *cmd, const char *name, const char **value)
{
    size_t name_len;

    if (cmd == NULL || name == NULL || value == NULL) {
        return false;
    }

    name_len = strlen(name);
    if (!prefix_equals_ignore_case(cmd, name)) {
        return false;
    }
    if (cmd[name_len] != '=') {
        return false;
    }

    *value = skip_whitespace(cmd + name_len + 1u);
    return true;
}

static char *config_next_token(char **cursor)
{
    char *start;
    char *end;

    if (cursor == NULL || *cursor == NULL) {
        return NULL;
    }

    start = *cursor;
    while (*start == ' ' || *start == '\t') {
        ++start;
    }
    if (*start == '\0') {
        *cursor = NULL;
        return NULL;
    }

    end = start;
    while (*end != '\0' && *end != ',') {
        ++end;
    }

    if (*end == ',') {
        *end = '\0';
        *cursor = end + 1;
    } else {
        *cursor = NULL;
    }

    trim_trailing(start);
    return start;
}

static void set_link_defaults(void)
{
    static const uint8_t zero_ip[4] = {0u, 0u, 0u, 0u};
    static const uint8_t c1_ip[4] = {192u, 168u, 1u, 200u};
    static const uint8_t c2_ip[4] = {192u, 168u, 1u, 201u};

    memset(g_config.links, 0, sizeof(g_config.links));

    g_config.links[CONFIG_LINK_S1].enabled = 1u;
    g_config.links[CONFIG_LINK_S1].uart = LINK_UART_U0;
    g_config.links[CONFIG_LINK_S1].local_port = 8080u;
    memcpy(g_config.links[CONFIG_LINK_S1].remote_ip, zero_ip, sizeof(zero_ip));

    g_config.links[CONFIG_LINK_S2].enabled = 0u;
    g_config.links[CONFIG_LINK_S2].uart = LINK_UART_U1;
    g_config.links[CONFIG_LINK_S2].local_port = 8081u;
    memcpy(g_config.links[CONFIG_LINK_S2].remote_ip, zero_ip, sizeof(zero_ip));

    g_config.links[CONFIG_LINK_C1].enabled = 1u;
    g_config.links[CONFIG_LINK_C1].uart = LINK_UART_U1;
    g_config.links[CONFIG_LINK_C1].local_port = 9001u;
    memcpy(g_config.links[CONFIG_LINK_C1].remote_ip, c1_ip, sizeof(c1_ip));
    g_config.links[CONFIG_LINK_C1].remote_port = 9000u;

    g_config.links[CONFIG_LINK_C2].enabled = 0u;
    g_config.links[CONFIG_LINK_C2].uart = LINK_UART_U0;
    g_config.links[CONFIG_LINK_C2].local_port = 9002u;
    memcpy(g_config.links[CONFIG_LINK_C2].remote_ip, c2_ip, sizeof(c2_ip));
    g_config.links[CONFIG_LINK_C2].remote_port = 9001u;
}

static void migrate_legacy_config(const legacy_device_config_v2_t *legacy)
{
    config_set_defaults();

    memcpy(g_config.net.mac, legacy->mac, sizeof(g_config.net.mac));
    memcpy(g_config.net.ip, legacy->ip, sizeof(g_config.net.ip));
    memcpy(g_config.net.mask, legacy->mask, sizeof(g_config.net.mask));
    memcpy(g_config.net.gw, legacy->gw, sizeof(g_config.net.gw));
    g_config.uart_baudrate[0] = legacy->uart2_baudrate;
    g_config.uart_baudrate[1] = legacy->uart3_baudrate;
    g_config.mux_mode = MUX_MODE_RAW;

    g_config.links[CONFIG_LINK_S1].enabled = (legacy->server_port != 0u) ? 1u : 0u;
    g_config.links[CONFIG_LINK_S1].uart = LINK_UART_U0;
    g_config.links[CONFIG_LINK_S1].local_port = legacy->server_port;
    memset(g_config.links[CONFIG_LINK_S1].remote_ip, 0, sizeof(g_config.links[CONFIG_LINK_S1].remote_ip));
    g_config.links[CONFIG_LINK_S1].remote_port = 0u;

    g_config.links[CONFIG_LINK_S2].enabled = 0u;

    g_config.links[CONFIG_LINK_C1].enabled = (legacy->remote_port != 0u) ? 1u : 0u;
    g_config.links[CONFIG_LINK_C1].uart = LINK_UART_U1;
    g_config.links[CONFIG_LINK_C1].local_port = 8081u;
    memcpy(g_config.links[CONFIG_LINK_C1].remote_ip, legacy->remote_ip, sizeof(g_config.links[CONFIG_LINK_C1].remote_ip));
    g_config.links[CONFIG_LINK_C1].remote_port = legacy->remote_port;

    g_config.links[CONFIG_LINK_C2].enabled = 0u;
    g_config.crc = config_calc_crc(&g_config);
}

static bool try_load_legacy_config(void)
{
    legacy_device_config_v2_t legacy;
    uint32_t expected_crc;

    if (flash_param_read(&legacy, sizeof(legacy)) != 0) {
        return false;
    }

    expected_crc = flash_param_crc32(&legacy, offsetof(legacy_device_config_v2_t, crc));
    if (legacy.magic != CONFIG_MAGIC || legacy.version != 0x0002u || legacy.crc != expected_crc) {
        return false;
    }

    migrate_legacy_config(&legacy);
    return true;
}

static at_result_t handle_summary_query(char *response, uint16_t max_len)
{
    char ip_str[16];
    char mask_str[16];
    char gw_str[16];
    char mac_str[18];
    char rip_str[CONFIG_LINK_COUNT][16];

    config_ip_to_str(g_config.net.ip, ip_str);
    config_ip_to_str(g_config.net.mask, mask_str);
    config_ip_to_str(g_config.net.gw, gw_str);
    config_mac_to_str(g_config.net.mac, mac_str);
    for (uint32_t i = 0; i < CONFIG_LINK_COUNT; ++i) {
        config_ip_to_str(g_config.links[i].remote_ip, rip_str[i]);
    }

    snprintf(response, max_len,
             "+NET:IP=%s,MASK=%s,GW=%s,MAC=%s\r\n"
             "+LINK:S1,EN=%u,LPORT=%u,RIP=%s,RPORT=%u,UART=%s\r\n"
             "+LINK:S2,EN=%u,LPORT=%u,RIP=%s,RPORT=%u,UART=%s\r\n"
             "+LINK:C1,EN=%u,LPORT=%u,RIP=%s,RPORT=%u,UART=%s\r\n"
             "+LINK:C2,EN=%u,LPORT=%u,RIP=%s,RPORT=%u,UART=%s\r\n"
             "+MUX:%u\r\n"
             "+MAP:UART2=0x04,UART3=0x08,C1=0x01,C2=0x02,S1=0x10,S2=0x20\r\n"
             "+BAUD:U0=%lu,U1=%lu\r\n"
             "OK\r\n",
             ip_str, mask_str, gw_str, mac_str,
             g_config.links[0].enabled, g_config.links[0].local_port, rip_str[0], g_config.links[0].remote_port, link_uart_to_str(g_config.links[0].uart),
             g_config.links[1].enabled, g_config.links[1].local_port, rip_str[1], g_config.links[1].remote_port, link_uart_to_str(g_config.links[1].uart),
             g_config.links[2].enabled, g_config.links[2].local_port, rip_str[2], g_config.links[2].remote_port, link_uart_to_str(g_config.links[2].uart),
             g_config.links[3].enabled, g_config.links[3].local_port, rip_str[3], g_config.links[3].remote_port, link_uart_to_str(g_config.links[3].uart),
             g_config.mux_mode,
             (unsigned long)g_config.uart_baudrate[0],
             (unsigned long)g_config.uart_baudrate[1]);

    return AT_OK;
}

static at_result_t handle_net_query(char *response, uint16_t max_len)
{
    char ip_str[16];
    char mask_str[16];
    char gw_str[16];
    char mac_str[18];

    config_ip_to_str(g_config.net.ip, ip_str);
    config_ip_to_str(g_config.net.mask, mask_str);
    config_ip_to_str(g_config.net.gw, gw_str);
    config_mac_to_str(g_config.net.mac, mac_str);
    snprintf(response, max_len, "+NET:IP=%s,MASK=%s,GW=%s,MAC=%s\r\nOK\r\n", ip_str, mask_str, gw_str, mac_str);
    return AT_OK;
}

static at_result_t handle_baud_query(char *response, uint16_t max_len)
{
    snprintf(response,
             max_len,
             "+BAUD:U0=%lu,U1=%lu\r\nOK\r\n",
             (unsigned long)g_config.uart_baudrate[0],
             (unsigned long)g_config.uart_baudrate[1]);
    return AT_OK;
}

static at_result_t handle_link_query(uint32_t index, char *response, uint16_t max_len)
{
    char rip_str[16];

    if (index >= CONFIG_LINK_COUNT) {
        snprintf(response, max_len, "ERROR: Invalid route field\r\n");
        return AT_INVALID_PARAM;
    }

    config_ip_to_str(g_config.links[index].remote_ip, rip_str);
    snprintf(response,
             max_len,
             "+LINK:%s,EN=%u,LPORT=%u,RIP=%s,RPORT=%u,UART=%s\r\nOK\r\n",
             link_index_to_name(index),
             g_config.links[index].enabled,
             g_config.links[index].local_port,
             rip_str,
             g_config.links[index].remote_port,
             link_uart_to_str(g_config.links[index].uart));
    return AT_OK;
}

static at_result_t handle_all_link_query(char *response, uint16_t max_len)
{
    char rip_str[CONFIG_LINK_COUNT][16];

    for (uint32_t i = 0; i < CONFIG_LINK_COUNT; ++i) {
        config_ip_to_str(g_config.links[i].remote_ip, rip_str[i]);
    }

    snprintf(response,
             max_len,
             "+LINK:S1,EN=%u,LPORT=%u,RIP=%s,RPORT=%u,UART=%s\r\n"
             "+LINK:S2,EN=%u,LPORT=%u,RIP=%s,RPORT=%u,UART=%s\r\n"
             "+LINK:C1,EN=%u,LPORT=%u,RIP=%s,RPORT=%u,UART=%s\r\n"
             "+LINK:C2,EN=%u,LPORT=%u,RIP=%s,RPORT=%u,UART=%s\r\n"
             "OK\r\n",
             g_config.links[0].enabled, g_config.links[0].local_port, rip_str[0], g_config.links[0].remote_port, link_uart_to_str(g_config.links[0].uart),
             g_config.links[1].enabled, g_config.links[1].local_port, rip_str[1], g_config.links[1].remote_port, link_uart_to_str(g_config.links[1].uart),
             g_config.links[2].enabled, g_config.links[2].local_port, rip_str[2], g_config.links[2].remote_port, link_uart_to_str(g_config.links[2].uart),
             g_config.links[3].enabled, g_config.links[3].local_port, rip_str[3], g_config.links[3].remote_port, link_uart_to_str(g_config.links[3].uart));
    return AT_OK;
}

int config_init(void)
{
    flash_param_init();
    return config_load();
}

int config_load(void)
{
    if (flash_param_read(&g_config, sizeof(g_config)) == 0 &&
        g_config.magic == CONFIG_MAGIC &&
        g_config.version == CONFIG_VERSION &&
        g_config.crc == config_calc_crc(&g_config)) {
        return 0;
    }

    if (try_load_legacy_config()) {
        return 0;
    }

    config_set_defaults();
    return -1;
}

int config_save(void)
{
    g_config.magic = CONFIG_MAGIC;
    g_config.version = CONFIG_VERSION;
    g_config.crc = config_calc_crc(&g_config);
    return flash_param_write(&g_config, sizeof(g_config));
}

void config_set_defaults(void)
{
    const uint8_t default_ip[] = DEFAULT_NET_IP;
    const uint8_t default_mask[] = DEFAULT_NET_MASK;
    const uint8_t default_gw[] = DEFAULT_NET_GW;
    const uint8_t default_mac[] = DEFAULT_NET_MAC;

    memset(&g_config, 0, sizeof(g_config));
    g_config.magic = CONFIG_MAGIC;
    g_config.version = CONFIG_VERSION;
    g_config.mux_mode = MUX_MODE_RAW;
    memcpy(g_config.net.ip, default_ip, sizeof(g_config.net.ip));
    memcpy(g_config.net.mask, default_mask, sizeof(g_config.net.mask));
    memcpy(g_config.net.gw, default_gw, sizeof(g_config.net.gw));
    memcpy(g_config.net.mac, default_mac, sizeof(g_config.net.mac));
    set_link_defaults();
    g_config.uart_baudrate[0] = DEFAULT_UART_BAUDRATE;
    g_config.uart_baudrate[1] = DEFAULT_UART_BAUDRATE;
    g_config.crc = config_calc_crc(&g_config);
}

const device_config_t *config_get(void)
{
    return &g_config;
}

device_config_t *config_get_mutable(void)
{
    return &g_config;
}

at_result_t config_process_at_cmd(const char *cmd, char *response, uint16_t max_len)
{
    const char *value;
    const char *p;

    if (cmd == NULL || response == NULL || max_len == 0u) {
        return AT_ERROR;
    }

    strncpy(g_cmd_work_buffer, cmd, sizeof(g_cmd_work_buffer) - 1u);
    g_cmd_work_buffer[sizeof(g_cmd_work_buffer) - 1u] = '\0';
    trim_trailing(g_cmd_work_buffer);
    p = skip_whitespace(g_cmd_work_buffer);

    if ((p[0] != 'A' && p[0] != 'a') || (p[1] != 'T' && p[1] != 't')) {
        snprintf(response, max_len, "ERROR: Unknown command\r\n");
        return AT_UNKNOWN_CMD;
    }
    if (p[2] == '\0') {
        snprintf(response, max_len, "OK\r\n");
        return AT_OK;
    }
    if (p[2] != '+') {
        snprintf(response, max_len, "ERROR: Unknown command\r\n");
        return AT_UNKNOWN_CMD;
    }

    p += 3;

    if ((equals_ignore_case(p, "?") || equals_ignore_case(p, "QUERY"))) {
        return handle_summary_query(response, max_len);
    }
    if (equals_ignore_case(p, "SAVE")) {
        if (config_save() != 0) {
            snprintf(response, max_len, "ERROR: Save failed\r\n");
            return AT_SAVE_FAILED;
        }
        snprintf(response, max_len, "OK: Configuration saved\r\n");
        return AT_OK;
    }
    if (equals_ignore_case(p, "RESET")) {
        g_reset_requested = true;
        snprintf(response, max_len, "OK: Resetting...\r\n");
        return AT_OK;
    }
    if (equals_ignore_case(p, "DEFAULT")) {
        config_set_defaults();
        snprintf(response, max_len, "OK: Defaults restored\r\n");
        return AT_OK;
    }
    if (equals_ignore_case(p, "MUX?")) {
        snprintf(response, max_len, "+MUX:%u\r\nOK\r\n", g_config.mux_mode);
        return AT_OK;
    }
    if (parse_command_with_value(p, "MUX", &value)) {
        uint32_t mux_value;
        if (parse_u32_value(value, 0u, 1u, &mux_value) != 0) {
            snprintf(response, max_len, "ERROR: Invalid value\r\n");
            return AT_INVALID_PARAM;
        }
        g_config.mux_mode = (uint8_t)mux_value;
        snprintf(response, max_len, "OK\r\n");
        return AT_NEED_REBOOT;
    }
    if (equals_ignore_case(p, "BAUD?")) {
        return handle_baud_query(response, max_len);
    }
    if (parse_command_with_value(p, "BAUD", &value)) {
        char value_copy[32];
        char *cursor;
        char *token;
        uint8_t uart;
        uint32_t baudrate;

        strncpy(value_copy, value, sizeof(value_copy) - 1u);
        value_copy[sizeof(value_copy) - 1u] = '\0';
        cursor = value_copy;

        token = config_next_token(&cursor);
        if (token == NULL || parse_link_uart(token, &uart) != 0) {
            snprintf(response, max_len, "ERROR: Invalid route field\r\n");
            return AT_INVALID_PARAM;
        }
        token = config_next_token(&cursor);
        if (token == NULL || parse_u32_value(token, 1200u, 921600u, &baudrate) != 0) {
            snprintf(response, max_len, "ERROR: Invalid baudrate\r\n");
            return AT_INVALID_PARAM;
        }
        if (config_next_token(&cursor) != NULL) {
            snprintf(response, max_len, "ERROR: Invalid value\r\n");
            return AT_INVALID_PARAM;
        }

        g_config.uart_baudrate[uart] = baudrate;
        return handle_baud_query(response, max_len) == AT_OK ? AT_NEED_REBOOT : AT_ERROR;
    }
    if (equals_ignore_case(p, "NET?")) {
        return handle_net_query(response, max_len);
    }
    if (parse_command_with_value(p, "NET", &value)) {
        char value_copy[96];
        char *token;
        char *cursor;
        uint8_t ip[4];
        uint8_t mask[4];
        uint8_t gw[4];
        uint8_t mac[6];

        strncpy(value_copy, value, sizeof(value_copy) - 1u);
        value_copy[sizeof(value_copy) - 1u] = '\0';
        cursor = value_copy;

        token = config_next_token(&cursor);
        if (token == NULL || config_str_to_ip(token, ip) != 0) {
            snprintf(response, max_len, "ERROR: Invalid IP format\r\n");
            return AT_INVALID_PARAM;
        }
        token = config_next_token(&cursor);
        if (token == NULL || config_str_to_ip(token, mask) != 0) {
            snprintf(response, max_len, "ERROR: Invalid mask format\r\n");
            return AT_INVALID_PARAM;
        }
        token = config_next_token(&cursor);
        if (token == NULL || config_str_to_ip(token, gw) != 0) {
            snprintf(response, max_len, "ERROR: Invalid gateway format\r\n");
            return AT_INVALID_PARAM;
        }
        token = config_next_token(&cursor);
        if (token == NULL || config_str_to_mac(token, mac) != 0) {
            snprintf(response, max_len, "ERROR: Invalid MAC format\r\n");
            return AT_INVALID_PARAM;
        }
        if (config_next_token(&cursor) != NULL) {
            snprintf(response, max_len, "ERROR: Invalid value\r\n");
            return AT_INVALID_PARAM;
        }

        memcpy(g_config.net.ip, ip, sizeof(ip));
        memcpy(g_config.net.mask, mask, sizeof(mask));
        memcpy(g_config.net.gw, gw, sizeof(gw));
        memcpy(g_config.net.mac, mac, sizeof(mac));
        snprintf(response, max_len, "OK\r\n");
        return AT_NEED_REBOOT;
    }
    if (equals_ignore_case(p, "LINK?")) {
        return handle_all_link_query(response, max_len);
    }
    if (parse_command_with_value(p, "LINK", &value)) {
        char value_copy[96];
        char *cursor;
        char *token;
        uint32_t index;
        uint32_t enabled;
        uint32_t local_port;
        uint32_t remote_port;
        uint8_t rip[4];
        uint8_t uart;

        strncpy(value_copy, value, sizeof(value_copy) - 1u);
        value_copy[sizeof(value_copy) - 1u] = '\0';
        cursor = value_copy;

        token = config_next_token(&cursor);
        if (token == NULL || parse_link_name(token, &index) != 0) {
            snprintf(response, max_len, "ERROR: Invalid route field\r\n");
            return AT_INVALID_PARAM;
        }
        token = config_next_token(&cursor);
        if (token == NULL) {
            return handle_link_query(index, response, max_len);
        }
        if (parse_u32_value(token, 0u, 1u, &enabled) != 0) {
            snprintf(response, max_len, "ERROR: Invalid value\r\n");
            return AT_INVALID_PARAM;
        }
        token = config_next_token(&cursor);
        if (token == NULL || parse_u32_value(token, 1u, 65535u, &local_port) != 0) {
            snprintf(response, max_len, "ERROR: Invalid port\r\n");
            return AT_INVALID_PARAM;
        }
        token = config_next_token(&cursor);
        if (token == NULL || config_str_to_ip(token, rip) != 0) {
            snprintf(response, max_len, "ERROR: Invalid remote IP format\r\n");
            return AT_INVALID_PARAM;
        }
        token = config_next_token(&cursor);
        if (token == NULL || parse_u32_value(token, 0u, 65535u, &remote_port) != 0) {
            snprintf(response, max_len, "ERROR: Invalid port\r\n");
            return AT_INVALID_PARAM;
        }
        token = config_next_token(&cursor);
        if (token == NULL || parse_link_uart(token, &uart) != 0) {
            snprintf(response, max_len, "ERROR: Invalid route field\r\n");
            return AT_INVALID_PARAM;
        }
        if (config_next_token(&cursor) != NULL) {
            snprintf(response, max_len, "ERROR: Invalid value\r\n");
            return AT_INVALID_PARAM;
        }

        g_config.links[index].enabled = (uint8_t)enabled;
        g_config.links[index].local_port = (uint16_t)local_port;
        memcpy(g_config.links[index].remote_ip, rip, sizeof(rip));
        g_config.links[index].remote_port = (uint16_t)remote_port;
        g_config.links[index].uart = uart;
        if (handle_link_query(index, response, max_len) != AT_OK) {
            snprintf(response, max_len, "ERROR: Invalid route field\r\n");
            return AT_INVALID_PARAM;
        }
        return AT_NEED_REBOOT;
    }

    snprintf(response, max_len, "ERROR: Unknown command\r\n");
    return AT_UNKNOWN_CMD;
}

void config_ip_to_str(const uint8_t *ip, char *str)
{
    sprintf(str, "%u.%u.%u.%u", ip[0], ip[1], ip[2], ip[3]);
}

int config_str_to_ip(const char *str, uint8_t *ip)
{
    int a;
    int b;
    int c;
    int d;

    if (sscanf(str, "%d.%d.%d.%d", &a, &b, &c, &d) != 4) {
        return -1;
    }
    if (a < 0 || a > 255 || b < 0 || b > 255 || c < 0 || c > 255 || d < 0 || d > 255) {
        return -1;
    }

    ip[0] = (uint8_t)a;
    ip[1] = (uint8_t)b;
    ip[2] = (uint8_t)c;
    ip[3] = (uint8_t)d;
    return 0;
}

void config_mac_to_str(const uint8_t *mac, char *str)
{
    sprintf(str, "%02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
}

int config_str_to_mac(const char *str, uint8_t *mac)
{
    int a[6];

    if (sscanf(str, "%x:%x:%x:%x:%x:%x", &a[0], &a[1], &a[2], &a[3], &a[4], &a[5]) != 6 &&
        sscanf(str, "%x-%x-%x-%x-%x-%x", &a[0], &a[1], &a[2], &a[3], &a[4], &a[5]) != 6) {
        return -1;
    }

    for (int i = 0; i < 6; ++i) {
        if (a[i] < 0 || a[i] > 255) {
            return -1;
        }
        mac[i] = (uint8_t)a[i];
    }
    return 0;
}

void config_poll(void)
{
    if (g_pending_cmd_ready) {
        uint16_t len = g_pending_cmd_len;
        g_pending_cmd_ready = false;
        g_pending_cmd_len = 0u;
        (void)config_try_process_frame((const uint8_t *)g_pending_cmd_buffer, len);
    }
}

void config_uart_rx_byte(uint8_t byte)
{
    if (byte == '\r') {
        g_uart_rx_seen_cr = true;
        return;
    }

    if (byte == '\n') {
        if (g_uart_rx_seen_cr && g_uart_cmd_len > 0u) {
            if (!g_pending_cmd_ready) {
                memcpy(g_pending_cmd_buffer, g_uart_cmd_buffer, g_uart_cmd_len);
                g_pending_cmd_buffer[g_uart_cmd_len] = '\0';
                g_pending_cmd_len = g_uart_cmd_len;
                g_pending_cmd_ready = true;
            }
            g_uart_cmd_len = 0u;
        }
        g_uart_rx_seen_cr = false;
        return;
    }

    if (g_uart_rx_seen_cr) {
        g_uart_cmd_len = 0u;
        g_uart_rx_seen_cr = false;
    }

    if (g_uart_cmd_len < (CONFIG_CMD_MAX_LEN - 1u)) {
        g_uart_cmd_buffer[g_uart_cmd_len++] = byte;
        g_uart_cmd_buffer[g_uart_cmd_len] = '\0';
    } else {
        g_uart_cmd_len = 0u;
    }
}

bool config_build_response_frame(const uint8_t *data,
                                 uint16_t len,
                                 char *response,
                                 uint16_t max_len,
                                 at_result_t *result)
{
    if (data == NULL || response == NULL || len < 2u || max_len == 0u) {
        return false;
    }
    if (len >= CONFIG_CMD_MAX_LEN) {
        return false;
    }

    memcpy(g_cmd_parse_buffer, data, len);
    g_cmd_parse_buffer[len] = '\0';
    if (((g_cmd_parse_buffer[0] != 'A') && (g_cmd_parse_buffer[0] != 'a')) ||
        ((g_cmd_parse_buffer[1] != 'T') && (g_cmd_parse_buffer[1] != 't'))) {
        return false;
    }

    *result = config_process_at_cmd(g_cmd_parse_buffer, response, max_len);
    return true;
}

bool config_try_process_frame(const uint8_t *data, uint16_t len)
{
    at_result_t result = AT_ERROR;

    if (!config_build_response_frame(data, len, g_at_response_buffer, sizeof(g_at_response_buffer), &result)) {
        return false;
    }

    if (HAL_UART_Transmit(&CONFIG_UART_HANDLE,
                          (uint8_t *)g_at_response_buffer,
                          (uint16_t)strlen(g_at_response_buffer),
                          1000u) != HAL_OK) {
        return false;
    }

    if (result == AT_NEED_REBOOT) {
        static const char hint[] = "Note: Use AT+SAVE then AT+RESET to apply changes\r\n";
        if (HAL_UART_Transmit(&CONFIG_UART_HANDLE,
                              (uint8_t *)hint,
                              sizeof(hint) - 1u,
                              1000u) != HAL_OK) {
            return false;
        }
    }

    return true;
}

bool config_is_reset_requested(void)
{
    return g_reset_requested;
}

void config_clear_reset_requested(void)
{
    g_reset_requested = false;
}

uint8_t config_link_index_to_endpoint(uint8_t index)
{
    switch (index) {
    case CONFIG_LINK_S1:
        return ENDPOINT_S1;
    case CONFIG_LINK_S2:
        return ENDPOINT_S2;
    case CONFIG_LINK_C1:
        return ENDPOINT_C1;
    case CONFIG_LINK_C2:
        return ENDPOINT_C2;
    default:
        return 0u;
    }
}

uint8_t config_uart_index_to_endpoint(uint8_t uart_index)
{
    return (uart_index == LINK_UART_U1) ? ENDPOINT_UART3 : ENDPOINT_UART2;
}

bool config_endpoint_is_single(uint8_t endpoint)
{
    return endpoint == ENDPOINT_C1 || endpoint == ENDPOINT_C2 ||
           endpoint == ENDPOINT_UART2 || endpoint == ENDPOINT_UART3 ||
           endpoint == ENDPOINT_S1 || endpoint == ENDPOINT_S2;
}
