/*
 * Ouroboros - Copyright (C) 2016 - 2026
 *
 * The API to instruct IPCPs
 *
 *    Dimitri Staessens <dimitri@ouroboros.rocks>
 *    Sander Vrijders   <sander@ouroboros.rocks>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2 as
 * published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., http://www.fsf.org/about/contact/.
 */

#define _POSIX_C_SOURCE 200112L

#include "config.h"

#define OUROBOROS_PREFIX "irmd/ipcp"

#include <ouroboros/errno.h>
#include <ouroboros/flow.h>
#include <ouroboros/logs.h>
#include <ouroboros/sockets.h>
#include <ouroboros/time.h>
#include <ouroboros/utils.h>

#include "ipcp.h"

#include <fcntl.h>
#include <pthread.h>
#include <signal.h>
#include <spawn.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/socket.h>
#include <sys/time.h>

static char * str_ipcp_cmd(int code)
{
        switch (code) {
        case IPCP_MSG_CODE__IPCP_BOOTSTRAP:
                return "bootstrap";
        case IPCP_MSG_CODE__IPCP_ENROLL:
                return "enroll";
        case IPCP_MSG_CODE__IPCP_CONNECT:
                return "connect";
        case IPCP_MSG_CODE__IPCP_DISCONNECT:
                return "disconnect";
        case IPCP_MSG_CODE__IPCP_REG:
                return "reg";
        case IPCP_MSG_CODE__IPCP_UNREG:
                return "unreg";
        case IPCP_MSG_CODE__IPCP_QUERY:
                return "query";
        case IPCP_MSG_CODE__IPCP_FLOW_JOIN:
                return "join";
        case IPCP_MSG_CODE__IPCP_FLOW_ALLOC:
                return "alloc";
        case IPCP_MSG_CODE__IPCP_FLOW_ALLOC_RESP:
                return "alloc_resp";
        case IPCP_MSG_CODE__IPCP_FLOW_DEALLOC:
                return "dealloc";
        default:
                assert(false);
                return "unknown";
        }
}

ipcp_msg_t * send_recv_ipcp_msg(pid_t        pid,
                                ipcp_msg_t * msg)
{
        int             sockfd;
        uint8_t         buf[SOCK_BUF_SIZE];
        char *          spath;
        ssize_t         len;
        struct timeval  tv;
        struct timespec tic;
        struct timespec toc;
        bool            may_fail = false;

        if (kill(pid, 0) < 0)
                return NULL;

        spath = sock_path(pid, IPCP_SOCK_PATH_PREFIX);
        if (spath == NULL) {
                log_err("Failed to get IPCP socket path for pid %d.", pid);
                return NULL;
        }

        sockfd = client_socket_open(spath);
        if (sockfd < 0) {
                log_err("Failed to open client socket at %s.", spath);
                free(spath);
                return NULL;
        }

        free(spath);

        len = ipcp_msg__get_packed_size(msg);
        if (len == 0 || len >= SOCK_BUF_SIZE) {
                log_warn("IPCP message has invalid size: %zd.", len);
                close(sockfd);
                return NULL;
        }

        switch (msg->code) {
        case IPCP_MSG_CODE__IPCP_BOOTSTRAP:
                tv.tv_sec  = BOOTSTRAP_TIMEOUT / 1000;
                tv.tv_usec = (BOOTSTRAP_TIMEOUT % 1000) * 1000;
                break;
        case IPCP_MSG_CODE__IPCP_ENROLL:
                tv.tv_sec  = ENROLL_TIMEOUT / 1000;
                tv.tv_usec = (ENROLL_TIMEOUT % 1000) * 1000;
                break;
        case IPCP_MSG_CODE__IPCP_REG:
                tv.tv_sec  = REG_TIMEOUT / 1000;
                tv.tv_usec = (REG_TIMEOUT % 1000) * 1000;
                break;
        case IPCP_MSG_CODE__IPCP_QUERY:
                may_fail = true; /* name not always in Layer */
                tv.tv_sec  = QUERY_TIMEOUT / 1000;
                tv.tv_usec = (QUERY_TIMEOUT % 1000) * 1000;
                break;
        case IPCP_MSG_CODE__IPCP_CONNECT:
                tv.tv_sec  = CONNECT_TIMEOUT / 1000;
                tv.tv_usec = (CONNECT_TIMEOUT % 1000) * 1000;
                break;
        case IPCP_MSG_CODE__IPCP_FLOW_ALLOC:
                tv.tv_sec  = FLOW_ALLOC_TIMEOUT / 1000;
                tv.tv_usec = (FLOW_ALLOC_TIMEOUT % 1000) * 1000;
                break;
        case IPCP_MSG_CODE__IPCP_FLOW_DEALLOC:
                may_fail = true;
                tv.tv_sec  = 0; /* FIX DEALLOC: don't wait for dealloc */
                tv.tv_usec = 500;
                break;
        default:
                tv.tv_sec  = SOCKET_TIMEOUT / 1000;
                tv.tv_usec = (SOCKET_TIMEOUT % 1000) * 1000;
                break;
        }

        if (setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO,
                        (void *) &tv, sizeof(tv)))
                log_warn("Failed to set timeout on socket.");

        pthread_cleanup_push(__cleanup_close_ptr, (void *) &sockfd);

        ipcp_msg__pack(msg, buf);

        clock_gettime(CLOCK_REALTIME, &tic);

        if (write(sockfd, buf, len) != -1)
                len = read(sockfd, buf, SOCK_BUF_SIZE);

        clock_gettime(CLOCK_REALTIME, &toc);

        pthread_cleanup_pop(true); /* close socket */

        if (len > 0)
                return ipcp_msg__unpack(NULL, len, buf);

        if (errno == EAGAIN && !may_fail) {
                int diff = ts_diff_ms(&toc, &tic);
                log_warn("IPCP %s timed out after %d ms.",
                         str_ipcp_cmd(msg->code), diff);
        }

        return NULL;
}

int ipcp_bootstrap(pid_t                pid,
                   struct ipcp_config * conf,
                   struct layer_info *  info)
{
        ipcp_msg_t   msg = IPCP_MSG__INIT;
        ipcp_msg_t * recv_msg;
        int          ret;

        if (conf == NULL)
                return -EINVAL;

        msg.code = IPCP_MSG_CODE__IPCP_BOOTSTRAP;
        msg.conf = ipcp_config_s_to_msg(conf);

        recv_msg = send_recv_ipcp_msg(pid, &msg);
        ipcp_config_msg__free_unpacked(msg.conf, NULL);
        if (recv_msg == NULL)
                return -EIPCP;

        if (!recv_msg->has_result) {
                ipcp_msg__free_unpacked(recv_msg, NULL);
                return -EIPCP;
        }

        ret = recv_msg->result;
        if (ret != 0) {
                ipcp_msg__free_unpacked(recv_msg, NULL);
                return ret;
        }

        if (recv_msg->layer_info == NULL) {
                ipcp_msg__free_unpacked(recv_msg, NULL);
                return -EIPCP;
        }

        info->dir_hash_algo = recv_msg->layer_info->dir_hash_algo;
        strcpy(info->name, recv_msg->layer_info->name);

        ret = recv_msg->result;
        ipcp_msg__free_unpacked(recv_msg, NULL);

        return ret;
}

int ipcp_enroll(pid_t               pid,
                const char *        dst,
                struct layer_info * info)
{
        ipcp_msg_t   msg = IPCP_MSG__INIT;
        ipcp_msg_t * recv_msg;
        int          ret;

        if (dst == NULL)
                return -EINVAL;

        msg.code = IPCP_MSG_CODE__IPCP_ENROLL;
        msg.dst  = (char *) dst;

        recv_msg = send_recv_ipcp_msg(pid, &msg);
        if (recv_msg == NULL)
                return -EIPCP;

        if (!recv_msg->has_result) {
                ipcp_msg__free_unpacked(recv_msg, NULL);
                return -EIPCP;
        }

        ret = recv_msg->result;
        if (ret != 0) {
                ipcp_msg__free_unpacked(recv_msg, NULL);
                return ret;
        }

        if (recv_msg->layer_info == NULL) {
                ipcp_msg__free_unpacked(recv_msg, NULL);
                return -EIPCP;
        }

        info->dir_hash_algo = recv_msg->layer_info->dir_hash_algo;
        strcpy(info->name, recv_msg->layer_info->name);

        ipcp_msg__free_unpacked(recv_msg, NULL);

        return 0;
}

int ipcp_connect(pid_t        pid,
                 const char * dst,
                 const char * component,
                 qosspec_t    qs)
{
        ipcp_msg_t    msg = IPCP_MSG__INIT;
        ipcp_msg_t *  recv_msg;
        int           ret;

        msg.code    = IPCP_MSG_CODE__IPCP_CONNECT;
        msg.dst     = (char *) dst;
        msg.comp    = (char *) component;
        msg.has_pid = true;
        msg.pid     = pid;
        msg.qosspec = qos_spec_s_to_msg(&qs);

        recv_msg = send_recv_ipcp_msg(pid, &msg);
        free(msg.qosspec);
        if (recv_msg == NULL)
                return -EIPCP;

        if (!recv_msg->has_result) {
                ipcp_msg__free_unpacked(recv_msg, NULL);
                return -EIPCP;
        }

        ret = recv_msg->result;
        ipcp_msg__free_unpacked(recv_msg, NULL);

        return ret;
}

int ipcp_disconnect(pid_t        pid,
                    const char * dst,
                    const char * component)
{
        ipcp_msg_t   msg = IPCP_MSG__INIT;
        ipcp_msg_t * recv_msg;
        int          ret;

        msg.code    = IPCP_MSG_CODE__IPCP_DISCONNECT;
        msg.dst     = (char *) dst;
        msg.comp    = (char *) component;
        msg.has_pid = true;
        msg.pid     = pid;

        recv_msg = send_recv_ipcp_msg(pid, &msg);
        if (recv_msg == NULL)
                return -EIPCP;

        if (!recv_msg->has_result) {
                ipcp_msg__free_unpacked(recv_msg, NULL);
                return -EIPCP;
        }

        ret = recv_msg->result;
        ipcp_msg__free_unpacked(recv_msg, NULL);

        return ret;
}

int ipcp_reg(pid_t          pid,
             const buffer_t hash)
{
        ipcp_msg_t   msg = IPCP_MSG__INIT;
        ipcp_msg_t * recv_msg;
        int          ret;

        msg.code      = IPCP_MSG_CODE__IPCP_REG;
        msg.has_hash  = true;
        msg.hash.data = (uint8_t *) hash.data;
        msg.hash.len  = hash.len;

        recv_msg = send_recv_ipcp_msg(pid, &msg);
        if (recv_msg == NULL)
                return -EIPCP;

        if (!recv_msg->has_result) {
                ipcp_msg__free_unpacked(recv_msg, NULL);
                return -EIPCP;
        }

        ret = recv_msg->result;
        ipcp_msg__free_unpacked(recv_msg, NULL);

        return ret;
}

int ipcp_unreg(pid_t          pid,
               const buffer_t hash)
{
        ipcp_msg_t   msg = IPCP_MSG__INIT;
        ipcp_msg_t * recv_msg;
        int          ret;

        msg.code      = IPCP_MSG_CODE__IPCP_UNREG;
        msg.has_hash  = true;
        msg.hash.data = (uint8_t *) hash.data;
        msg.hash.len  = hash.len;

        recv_msg = send_recv_ipcp_msg(pid, &msg);
        if (recv_msg == NULL)
                return -EIPCP;

        if (!recv_msg->has_result) {
                ipcp_msg__free_unpacked(recv_msg, NULL);
                return -EIPCP;
        }

        ret = recv_msg->result;
        ipcp_msg__free_unpacked(recv_msg, NULL);

        return ret;
}

int ipcp_query(pid_t          pid,
               const buffer_t dst)
{
        ipcp_msg_t   msg = IPCP_MSG__INIT;
        ipcp_msg_t * recv_msg;
        int          ret;

        msg.code      = IPCP_MSG_CODE__IPCP_QUERY;
        msg.has_hash  = true;
        msg.hash.data = (uint8_t *) dst.data;
        msg.hash.len  = dst.len;

        recv_msg = send_recv_ipcp_msg(pid, &msg);
        if (recv_msg == NULL)
                return -EIPCP;

        if (!recv_msg->has_result) {
                ipcp_msg__free_unpacked(recv_msg, NULL);
                return -EIPCP;
        }

        ret = recv_msg->result;
        ipcp_msg__free_unpacked(recv_msg, NULL);

        return ret;
}

int ipcp_flow_join(const struct flow_info * flow,
                   const buffer_t           dst)
{
        ipcp_msg_t   msg = IPCP_MSG__INIT;
        ipcp_msg_t * recv_msg;
        int          ret;

        msg.code         = IPCP_MSG_CODE__IPCP_FLOW_JOIN;
        msg.has_flow_id  = true;
        msg.flow_id      = flow->id;
        msg.has_pid      = true;
        msg.pid          = flow->n_pid;
        msg.has_uid      = true;
        msg.uid          = flow->uid;
        msg.has_hash     = true;
        msg.hash.data    = (uint8_t *) dst.data;
        msg.hash.len     = dst.len;
        msg.has_pk       = false;

        recv_msg = send_recv_ipcp_msg(flow->n_1_pid, &msg);
        free(msg.qosspec);
        if (recv_msg == NULL)
                return -EIPCP;

        if (!recv_msg->has_result) {
                ipcp_msg__free_unpacked(recv_msg, NULL);
                return -EIPCP;
        }

        ret = recv_msg->result;
        ipcp_msg__free_unpacked(recv_msg, NULL);

        return ret;
}

int ipcp_flow_update(const struct flow_info * flow,
                     const buffer_t           data)
{
        ipcp_msg_t   msg = IPCP_MSG__INIT;
        ipcp_msg_t * recv_msg;
        int          ret;

        msg.code         = IPCP_MSG_CODE__IPCP_FLOW_UPDATE;
        msg.has_flow_id  = true;
        msg.flow_id      = flow->id;
        msg.has_pk       = true;
        msg.pk.data      = data.data;
        msg.pk.len       = data.len;

        recv_msg = send_recv_ipcp_msg(flow->n_1_pid, &msg);
        if (recv_msg == NULL) {
                log_err("Did not receive message.");
                return -EIPCP;
        }

        if (!recv_msg->has_result) {
                log_err("Message has no result");
                ipcp_msg__free_unpacked(recv_msg, NULL);
                return -EIPCP;
        }

        ret = recv_msg->result;
        ipcp_msg__free_unpacked(recv_msg, NULL);

        return ret;
}

int ipcp_flow_alloc(const struct flow_info * flow,
                    const buffer_t           dst,
                    const buffer_t           data)
{
        ipcp_msg_t   msg = IPCP_MSG__INIT;
        ipcp_msg_t * recv_msg;
        int          ret;

        msg.code         = IPCP_MSG_CODE__IPCP_FLOW_ALLOC;
        msg.has_flow_id  = true;
        msg.flow_id      = flow->id;
        msg.has_pid      = true;
        msg.pid          = flow->n_pid;
        msg.has_uid      = true;
        msg.uid          = flow->uid;
        msg.qosspec      = qos_spec_s_to_msg(&flow->qs);
        msg.has_hash     = true;
        msg.hash.data    = (uint8_t *) dst.data;
        msg.hash.len     = dst.len;
        msg.has_pk       = true;
        msg.pk.data      = data.data;
        msg.pk.len       = data.len;

        recv_msg = send_recv_ipcp_msg(flow->n_1_pid, &msg);
        free(msg.qosspec);
        if (recv_msg == NULL) {
                log_err("Did not receive message.");
                return -EIPCP;
        }

        if (!recv_msg->has_result) {
                log_err("Message has no result");
                ipcp_msg__free_unpacked(recv_msg, NULL);
                return -EIPCP;
        }

        ret = recv_msg->result;
        ipcp_msg__free_unpacked(recv_msg, NULL);

        return ret;
}

int ipcp_flow_alloc_resp(const struct flow_info * flow,
                         int                      response,
                         const buffer_t           data)
{
        ipcp_msg_t   msg = IPCP_MSG__INIT;
        ipcp_msg_t * recv_msg;
        int          ret;

        msg.code         = IPCP_MSG_CODE__IPCP_FLOW_ALLOC_RESP;
        msg.has_flow_id  = true;
        msg.flow_id      = flow->id;
        msg.has_pid      = true;
        msg.pid          = flow->n_pid;
        msg.has_uid      = true;
        msg.uid          = flow->uid;
        msg.has_response = true;
        msg.response     = response;
        msg.has_pk       = response == 0;
        msg.pk.data      = data.data;
        msg.pk.len       = data.len;

        recv_msg = send_recv_ipcp_msg(flow->n_1_pid, &msg);
        if (recv_msg == NULL)
                return -EIPCP;

        if (!recv_msg->has_result) {
                ipcp_msg__free_unpacked(recv_msg, NULL);
                return -EIPCP;
        }

        ret = recv_msg->result;
        ipcp_msg__free_unpacked(recv_msg, NULL);

        return ret;
}

int ipcp_flow_dealloc(pid_t  pid,
                      int    flow_id,
                      time_t timeo)
{
        ipcp_msg_t   msg = IPCP_MSG__INIT;
        ipcp_msg_t * recv_msg;
        int          ret;

        msg.code          = IPCP_MSG_CODE__IPCP_FLOW_DEALLOC;
        msg.has_flow_id   = true;
        msg.flow_id       = flow_id;
        msg.has_timeo_sec = true;
        msg.timeo_sec     = timeo;

        recv_msg = send_recv_ipcp_msg(pid, &msg);
        if (recv_msg == NULL)
                return 0;

        if (!recv_msg->has_result) {
                ipcp_msg__free_unpacked(recv_msg, NULL);
                return 0;
        }

        ret = recv_msg->result;
        ipcp_msg__free_unpacked(recv_msg, NULL);

        return ret;
}
