/*
 * Ouroboros - Copyright (C) 2016 - 2017
 *
 * Enrollment Task
 *
 *    Dimitri Staessens <dimitri.staessens@ugent.be>
 *    Sander Vrijders   <sander.vrijders@ugent.be>
 *
 * 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 199309L

#define OUROBOROS_PREFIX "enrollment"

#include <ouroboros/endian.h>
#include <ouroboros/errno.h>
#include <ouroboros/time_utils.h>
#include <ouroboros/dev.h>
#include <ouroboros/logs.h>
#include <ouroboros/errno.h>
#include <ouroboros/sockets.h>

#include "connmgr.h"
#include "enroll.h"
#include "ipcp.h"

#include <assert.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>

#include "enroll.pb-c.h"
typedef EnrollMsg enroll_msg_t;

#define ENROLL_AE               "Enrollment"
#define ENROLL_PROTO            "OEP" /* Ouroboros enrollment protocol */
#define ENROLL_WARN_TIME_OFFSET 20
#define ENROLL_BUF_LEN          1024

enum enroll_state {
        ENROLL_NULL = 0,
        ENROLL_INIT,
        ENROLL_RUNNING
};

struct {
        struct ipcp_config conf;
        enum enroll_state  state;
        pthread_t          listener;
} enroll;

static int send_rcv_enroll_msg(int fd)
{
        enroll_msg_t    req = ENROLL_MSG__INIT;
        enroll_msg_t *  reply;
        uint8_t         buf[ENROLL_BUF_LEN];
        ssize_t         len;
        ssize_t         delta_t;
        struct timespec t0;
        struct timespec rtt;

        req.code = ENROLL_CODE__ENROLL_REQ;

        len = enroll_msg__get_packed_size(&req);
        if (len < 0) {
                log_dbg("Failed pack request message.");
                return -1;
        }

        enroll_msg__pack(&req, buf);

        clock_gettime(CLOCK_REALTIME, &t0);

        if (flow_write(fd, buf, len)) {
                log_dbg("Failed to send request message.");
                return -1;
        }

        len = flow_read(fd, buf, ENROLL_BUF_LEN);
        if (len < 0) {
                log_dbg("No enrollment reply received.");
                return -1;
        }

        log_dbg("Received enrollment info (%zd bytes).", len);

        reply = enroll_msg__unpack(NULL, len, buf);
        if (reply == NULL) {
                log_dbg("No enrollment response.");
                return -1;
        }

        if (reply->code != ENROLL_CODE__ENROLL_BOOT) {
                log_dbg("Failed to unpack enrollment response.");
                enroll_msg__free_unpacked(reply, NULL);
                return -1;
        }

        if (!(reply->has_t_sec && reply->has_t_nsec)) {
                log_dbg("No time in response message.");
                enroll_msg__free_unpacked(reply, NULL);
                return -1;
        }

        clock_gettime(CLOCK_REALTIME, &rtt);

        delta_t = ts_diff_ms(&t0, &rtt);

        rtt.tv_sec  = reply->t_sec;
        rtt.tv_nsec = reply->t_nsec;

        if (labs(ts_diff_ms(&t0, &rtt)) - delta_t > ENROLL_WARN_TIME_OFFSET)
                log_warn("Clock offset above threshold.");

        strcpy(enroll.conf.dif_info.dif_name, reply->conf->dif_info->dif_name);
        enroll.conf.type           = reply->conf->ipcp_type;
        enroll.conf.addr_size      = reply->conf->addr_size;
        enroll.conf.fd_size        = reply->conf->fd_size;
        enroll.conf.has_ttl        = reply->conf->has_ttl;
        enroll.conf.addr_auth_type = reply->conf->addr_auth_type;
        enroll.conf.routing_type   = reply->conf->routing_type;
        enroll.conf.pff_type       = reply->conf->pff_type;
        enroll.conf.dif_info.dir_hash_algo
                = reply->conf->dif_info->dir_hash_algo;

        enroll_msg__free_unpacked(reply, NULL);

        return 0;
}

static ssize_t enroll_pack(uint8_t ** buf)
{
        enroll_msg_t      msg      = ENROLL_MSG__INIT;
        ipcp_config_msg_t config   = IPCP_CONFIG_MSG__INIT;
        dif_info_msg_t    dif_info = DIF_INFO_MSG__INIT;
        struct timespec   now;
        ssize_t           len;

        clock_gettime(CLOCK_REALTIME, &now);

        msg.code       = ENROLL_CODE__ENROLL_BOOT;
        msg.has_t_sec  = true;
        msg.t_sec      = now.tv_sec;
        msg.has_t_nsec = true;
        msg.t_nsec     = now.tv_nsec;
        msg.conf       = &config;

        config.ipcp_type          = enroll.conf.type;
        config.has_addr_size      = true;
        config.addr_size          = enroll.conf.addr_size;
        config.has_fd_size        = true;
        config.fd_size            = enroll.conf.fd_size;
        config.has_has_ttl        = true;
        config.has_ttl            = enroll.conf.has_ttl;
        config.has_addr_auth_type = true;
        config.addr_auth_type     = enroll.conf.addr_auth_type;
        config.has_routing_type   = true;
        config.routing_type       = enroll.conf.routing_type;
        config.has_pff_type       = true;
        config.pff_type           = enroll.conf.pff_type;
        config.dif_info           = &dif_info;

        dif_info.dif_name      = (char *) enroll.conf.dif_info.dif_name;
        dif_info.dir_hash_algo = enroll.conf.dif_info.dir_hash_algo;

        len = enroll_msg__get_packed_size(&msg);

        *buf = malloc(len);
        if (*buf == NULL)
                return -1;

        enroll_msg__pack(&msg, *buf);

        return len;
}

static void * enroll_handle(void * o)
{
        struct conn    conn;
        uint8_t        buf[ENROLL_BUF_LEN];
        uint8_t *      reply;
        ssize_t        len;
        enroll_msg_t * msg;

        (void) o;

        while (true) {
                if (connmgr_wait(AEID_ENROLL, &conn)) {
                        log_err("Failed to get next connection.");
                        continue;
                }

                len = flow_read(conn.flow_info.fd, buf, ENROLL_BUF_LEN);
                if (len < 0) {
                        log_err("Failed to read from flow.");
                        connmgr_dealloc(AEID_ENROLL, &conn);
                        continue;
                }

                msg = enroll_msg__unpack(NULL, len, buf);
                if (msg == NULL) {
                        log_err("Failed to unpack message.");
                        connmgr_dealloc(AEID_ENROLL, &conn);
                        continue;
                }

                if (msg->code != ENROLL_CODE__ENROLL_REQ) {
                        log_err("Wrong message type.");
                        connmgr_dealloc(AEID_ENROLL, &conn);
                        enroll_msg__free_unpacked(msg, NULL);
                        continue;
                }

                log_dbg("Enrolling a new neighbor.");

                enroll_msg__free_unpacked(msg, NULL);

                len = enroll_pack(&reply);
                if (reply == NULL) {
                        log_err("Failed to pack enrollment message.");
                        connmgr_dealloc(AEID_ENROLL, &conn);
                        continue;
                }

                log_dbg("Sending enrollment info (%zd bytes).", len);

                if (flow_write(conn.flow_info.fd, reply, len)) {
                        log_err("Failed respond to enrollment request.");
                        connmgr_dealloc(AEID_ENROLL, &conn);
                        free(reply);
                        continue;
                }

                free(reply);

                len = flow_read(conn.flow_info.fd, buf, ENROLL_BUF_LEN);
                if (len < 0) {
                        log_err("Failed to read from flow.");
                        connmgr_dealloc(AEID_ENROLL, &conn);
                        continue;
                }

                msg = enroll_msg__unpack(NULL, len, buf);
                if (msg == NULL) {
                        log_err("Failed to unpack message.");
                        connmgr_dealloc(AEID_ENROLL, &conn);
                        continue;
                }

                if (msg->code != ENROLL_CODE__ENROLL_DONE || !msg->has_result) {
                        log_err("Wrong message type.");
                        enroll_msg__free_unpacked(msg, NULL);
                        connmgr_dealloc(AEID_ENROLL, &conn);
                        continue;
                }

                if (msg->result == 0)
                        log_dbg("Neighbor enrollment successful.");
                else
                        log_dbg("Neigbor reported failed enrollment.");

                enroll_msg__free_unpacked(msg, NULL);

                connmgr_dealloc(AEID_ENROLL, &conn);
        }

        return 0;
}

int enroll_boot(struct conn * conn)
{
        log_dbg("Getting boot information.");

        if (send_rcv_enroll_msg(conn->flow_info.fd)) {
                log_err("Failed to enroll.");
                return -1;
        }

        return 0;
}

int enroll_done(struct conn * conn,
                int           result)
{
        enroll_msg_t msg = ENROLL_MSG__INIT;
        uint8_t      buf[ENROLL_BUF_LEN];
        ssize_t       len;

        msg.code       = ENROLL_CODE__ENROLL_DONE;
        msg.has_result = true;
        msg.result     = result;

        len = enroll_msg__get_packed_size(&msg);
        if (len < 0) {
                log_dbg("Failed pack request message.");
                return -1;
        }

        enroll_msg__pack(&msg, buf);

        if (flow_write(conn->flow_info.fd, buf, len)) {
                log_dbg("Failed to send acknowledgment.");
                return -1;
        }

        return 0;
}

void enroll_bootstrap(const struct ipcp_config * conf)
{
        assert(conf);

        memcpy(&enroll.conf, conf, sizeof(enroll.conf));
}

struct ipcp_config * enroll_get_conf(void)
{
        return &enroll.conf;
}

int enroll_init(void)
{
        struct conn_info info;

        memset(&info, 0, sizeof(info));

        strcpy(info.ae_name, ENROLL_AE);
        strcpy(info.protocol, ENROLL_PROTO);
        info.pref_version = 1;
        info.pref_syntax  = PROTO_GPB;
        info.addr         = 0;

        if (connmgr_ae_init(AEID_ENROLL, &info)) {
                log_err("Failed to register with connmgr.");
                return -1;
        }

        enroll.state = ENROLL_INIT;

        return 0;
}

void enroll_fini(void)
{
        if (enroll.state == ENROLL_RUNNING)
                pthread_join(enroll.listener, NULL);

        connmgr_ae_fini(AEID_ENROLL);
}

int enroll_start(void)
{
        if (pthread_create(&enroll.listener, NULL, enroll_handle, NULL))
                return -1;

        enroll.state = ENROLL_RUNNING;

        return 0;
}

void enroll_stop(void)
{
        if (enroll.state == ENROLL_RUNNING)
                pthread_cancel(enroll.listener);
}