/*
 * Copyright (C) MOXA Inc. All rights reserved.
 * Authors:
 *     2024  Wilson YS Huang  <wilsonys.huang@moxa.com>
 * This software is distributed under the terms of the MOXA SOFTWARE NOTICE.
 * See the file LICENSE for details.
 */

#include <errno.h>
#include <signal.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/poll.h>
#include <sys/socket.h>
#include <termios.h>
#include <time.h>
#include <unistd.h>

#include <common.h>
#include <logger.h>
#include <mcu.h>
#include <socket.h>
#include <tty.h>

typedef struct {
    const char *model_name;
    const char *mcu_tty_device;
    size_t      name_prefix_sz;
    int32_t     mcu_features;
} model_information;

typedef void (*model_init_func)(const model_information *info);

typedef struct {
    model_information info;
    model_init_func   init_func;
} model_t;

static bool mcu_capability = true;

static void V3000_init_func(const model_information *info)
{
    const uint32_t IO_BOARD_GPIO_PIN = 47;
    const uint32_t IO_BOARD_4LAN     = 1;
    const uint32_t IO_BOARD_8LAN     = 0;
    int32_t        gpio_val          = -1;
    int32_t        gpio_base         = -1;

    (void)info;

    if (!get_gpio_base_by_label("gpio_it87", &gpio_base)) {
        log_error("Get gpio_it87 base failed");
        return;
    }

    if (!get_gpio_value(gpio_base + IT87_GPIO_MAPPING(IO_BOARD_GPIO_PIN), &gpio_val)) {
        log_error("Get IO board gpio value failed");
        return;
    }

    if (gpio_val == IO_BOARD_4LAN) {
        mcu_capability = false;
        log_warn("MCU is not available on 4 LAN IO board");
    }
    else if (gpio_val == IO_BOARD_8LAN) {
        mcu_capability = true;
    }
    else {
        log_error("Unknown IO board");
    }
}

static const model_t support_models[] = {
    {.info = {
         .model_name     = "V3000",   // In fact, it is V3200 series.
         .mcu_tty_device = "/dev/ttyS2",
         .name_prefix_sz = 5,
         .mcu_features   = FEAT_MCU_VERSION | FEAT_MCU_RELAY_MODE | FEAT_MCU_WDT_RELAY_MODE | FEAT_MCU_POWEROFF_RELAY_MODE | FEAT_MCU_APP_WDT_RELAY_MODE | FEAT_MCU_APP_WDT_TIMEOUT_CMD | FEAT_MCU_FW_UPGRADE_CMD,
     },
     .init_func = V3000_init_func},
    {.info = {
         .model_name     = "V3400",
         .mcu_tty_device = "/dev/ttyS2",
         .name_prefix_sz = 5,
         .mcu_features   = FEAT_MCU_VERSION | FEAT_MCU_RELAY_MODE | FEAT_MCU_WDT_RELAY_MODE | FEAT_MCU_POWEROFF_RELAY_MODE | FEAT_MCU_APP_WDT_RELAY_MODE | FEAT_MCU_APP_WDT_TIMEOUT_CMD | FEAT_MCU_FW_UPGRADE_CMD,
     },
     .init_func = V3000_init_func},
    {{NULL, NULL, 0, 0}, NULL}};

static const model_t *current_model  = NULL;
static int32_t        unix_server_fd = -1;

void handle_unix_response(mcu_error_code code, uint8_t *resp_data, size_t resp_data_sz, void *arg)
{
    int32_t                unix_cli_fd = *(int32_t *)arg;
    socket_response_packet resp_pkt    = {0};
    ssize_t                rc          = -1;

    memcpy(resp_pkt.payload.data, resp_data, resp_data_sz);
    resp_pkt.return_code     = code;
    resp_pkt.payload.data_sz = resp_data_sz;

    rc = write(unix_cli_fd, &resp_pkt, sizeof(socket_response_packet));
    if (rc == 0) {
        log_warn("Unix client disconnected gracefully.");
    }
    else if (rc < 0 && (errno == ECONNRESET || errno == EPIPE)) {
        log_error("Unix disconnected forcefully.");
    }
}

int32_t handle_unix_client()
{
    int32_t                unix_cli_fd = -1;
    socket_request_packet  req_pkt     = {0};
    socket_response_packet resp_pkt    = {0};
    ssize_t                rc          = -1;

    struct pollfd fds[10];
    int32_t       nfds = 1;

    fds[0].fd     = unix_server_fd;
    fds[0].events = POLLIN;

    log_info("Start handling unix client...");

    while (1) {
        errno = 0;
        rc    = poll(fds, nfds, -1);
        if (rc < 0) {
            log_warn("%s(%d)\n", strerror(errno), errno);
            if (errno == EINTR) {
                continue;
            }
            break;
        }

        if (fds[0].revents & POLLIN) {
            errno       = 0;
            unix_cli_fd = accept(unix_server_fd, NULL, NULL);

            if (is_valid_fd(unix_cli_fd)) {
                log_debug("Unix client connected(%d)", unix_cli_fd);
                fds[nfds].fd     = unix_cli_fd;
                fds[nfds].events = POLLIN | POLLHUP;
                nfds++;
            }
            else {
                log_warn("Invaild unix client. %s(%d)", strerror(errno), errno);
            }
        }

        for (int32_t i = 1; i < nfds; i++) {
            if (fds[i].revents & (POLLIN | POLLHUP)) {
                rc = read(fds[i].fd, &req_pkt, sizeof(req_pkt));
                if (rc <= 0) {
                    log_debug("Unix client disconnected(%d).", fds[i].fd);
                    close_fd(fds[i].fd);
                    fds[i] = fds[nfds - 1];
                    nfds--;
                    break;
                }

                if (mcu_capability) {
                    if (is_mcu_command_support(req_pkt.command, current_model->info.mcu_features)) {
                        mcu_hal_request(req_pkt.command, req_pkt.payload.data, req_pkt.payload.data_sz, handle_unix_response, &fds[i].fd);
                    }
                    else {
                        log_warn("MCU command is not support");
                        resp_pkt.return_code     = ERR_CODE_COMMAND_NOT_SUPPORT;
                        resp_pkt.payload.data_sz = 0;
                        rc                       = write(fds[i].fd, &resp_pkt, sizeof(socket_response_packet));
                        if (rc <= 0) {
                            log_warn("Response unix client failed.");
                        }
                    }
                }
                else {
                    resp_pkt.return_code     = ERR_CODE_MCU_NOT_AVAILABLE;
                    resp_pkt.payload.data_sz = 0;
                    rc                       = write(fds[i].fd, &resp_pkt, sizeof(socket_response_packet));
                    if (rc <= 0) {
                        log_warn("Response unix client failed.");
                    }
                }
            }
        }
    }

    log_info("Stop handling unix client");
    return 0;
}

void daemon_shutdown()
{
    log_info("Daemon shutdown.");
    unlink(MX_MCU_DAEMON_UNIX_SOCK_PATH);
    logger_shutdown();
}

void handle_daemon_reload(int32_t signal)
{
    (void)signal;

    if (logger_get_level() != LOG_DEBUG) {
        log_info("Enable debug log");
        logger_set_level(LOG_DEBUG);
    }
    else {
        log_info("Disable debug log");
        logger_set_level(LOG_INFO);
    }
}

void handle_daemon_end(int32_t signal)
{
    (void)signal;

    log_info("Receive SIGINT or SIGTERM signal.");

    daemon_shutdown();
    exit(EXIT_SUCCESS);
}

bool setup_signals()
{
    int32_t          rc = 0;
    struct sigaction sa = {0};

    for (int s = SIGRTMAX; s; s--) {
        switch (s) {
        case SIGHUP:
            sa.sa_handler = handle_daemon_reload;
            // If accept() is interrupted by SIGHUP, then the call is automatically restarted after the handle_daemon_reload returns
            sa.sa_flags = SA_RESTART;
            break;
        case SIGINT:
        case SIGTERM:
            sa.sa_handler = handle_daemon_end;
            break;
        default:
            continue;
        }
        if ((rc = sigaction(s, &sa, NULL)) == -1)
            return false;
    }

    return true;
}

bool identify_model()
{
    char buffer[64];

    char *cmd1[] = {"/usr/sbin/dmidecode", "-t", "12", NULL};
    char *cmd2[] = {"grep", "Option", NULL};
    char *cmd3[] = {"awk", "-F", ":", "{print substr($2,1,11)}", NULL};
    char *cmd4[] = {"sed", "s/ //g", NULL};
    char *cmd5[] = {"tr", "-d", "'[:space:]'", NULL};

    char **commands[] = {cmd1, cmd2, cmd3, cmd4, cmd5};

    if (!execute_pipeline(commands, sizeof(commands) / sizeof(commands[0]), buffer)) {
        log_error("Error: get model name failed\n");
        return false;
    }

    for (int32_t i = 0; support_models[i].info.model_name != NULL; ++i) {
        if (!strncmp(buffer, support_models[i].info.model_name, support_models[i].info.name_prefix_sz)) {
            current_model = &support_models[i];
            return true;
        }
    }

    return false;
}

bool daemon_init()
{
    logger_init("mx-mcud");
    log_info("Daemon init.");

    if (!setup_signals()) {
        log_error("Setup daemon signals handler failed.");
    }

    if (!identify_model()) {
        log_error("Unknown model.");
        return false;
    }

    log_info("The model %s is support.", current_model->info.model_name);

    current_model->init_func(&current_model->info);

    return true;
}

int32_t main(void)
{

    if (!daemon_init()) {
        log_error("Daemon initialization failed.");
        exit(EXIT_FAILURE);
    }

    unix_server_fd = create_unix_skt_server(MX_MCU_DAEMON_UNIX_SOCK_PATH);
    if (unix_server_fd < 0) {
        log_error("Unix socket server initialization failed.");
        exit(EXIT_FAILURE);
    }

    if (mcu_capability) {
        if (!mcu_hal_init(current_model->info.mcu_tty_device)) {
            log_error("MCU HAL initialization failed.");
        }
    }

    handle_unix_client();

    daemon_shutdown();

    return 0;
}