#include "internal.h"

#include <stdio.h>
#include <stdbool.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <time.h>
#include <sys/fcntl.h>
#include <sys/socket.h>
#include <sys/ioctl.h>
#include <arpa/inet.h>

#include <osmocom/core/select.h>
#include <osmocom/core/bitvec.h>
#include <osmocom/gsm/tlv.h>
#include <osmocom/core/msgb.h>
#include <osmocom/core/logging.h>
#include <osmocom/core/talloc.h>
#include <osmocom/abis/e1_input.h>
#include <osmocom/abis/ipaccess.h>
#include <osmocom/core/socket.h>

#include <osmocom/abis/ipa.h>
#include <osmocom/vty/vty.h>
#include <osmocom/vty/command.h>

static void *tall_ipa_proxy_ctx;

/*
 * data structures used by the IPA VTY commands
 */
static LLIST_HEAD(ipa_instance_list);

enum ipa_proxy_instance_net_type {
	IPA_INSTANCE_T_NONE,
	IPA_INSTANCE_T_BIND,
	IPA_INSTANCE_T_CONNECT,
	IPA_INSTANCE_T_MAX
};

struct ipa_proxy_instance_net {
	char					*addr;
	uint16_t				port;
	enum ipa_proxy_instance_net_type	type;
};

struct ipa_proxy_instance {
	struct llist_head		head;
#define IPA_INSTANCE_NAME		16
	char				name[IPA_INSTANCE_NAME];
	struct ipa_proxy_instance_net	net;
	int				refcnt;
};

static LLIST_HEAD(ipa_proxy_route_list);

/* Several routes pointing to the same instances share this. */
struct ipa_proxy_route_shared {
	int					refcnt;

	/* this file descriptor is used to accept() new connections. */
	struct osmo_fd				bfd;

	struct {
		struct ipa_proxy_instance	*inst;
		struct bitvec			streamid_map;
		uint8_t				streamid_map_data[(0xff+1)/8];
		uint8_t				streamid[0xff + 1];
	} src;
	struct {
		struct ipa_proxy_instance	*inst;
		struct bitvec			streamid_map;
		uint8_t				streamid_map_data[(0xff+1)/8];
		uint8_t				streamid[0xff + 1];
	} dst;

	struct llist_head			conn_list;
};

/* One route is composed of two instances. */
struct ipa_proxy_route {
	struct llist_head			head;

	struct {
		uint8_t				streamid;
	} src;
	struct {
		uint8_t				streamid;
	} dst;

	struct ipa_proxy_route_shared		*shared;
};

enum ipa_conn_state {
	IPA_CONN_S_NONE,
	IPA_CONN_S_CONNECTING,
	IPA_CONN_S_CONNECTED,
	IPA_CONN_S_MAX
};

/* One route may forward more than one connection. */
struct ipa_proxy_conn {
	struct llist_head		head;

	struct ipa_server_conn		*src;
	struct ipa_client_conn		*dst;
	struct ipa_proxy_route		*route;
};

/*
 * socket callbacks used by IPA VTY commands
 */
static int ipa_sock_dst_cb(struct ipa_client_conn *link, struct msgb *msg)
{
	struct ipaccess_head *hh;
	struct ipa_proxy_conn *conn = link->data;

	LOGP(DLINP, LOGL_NOTICE, "received message from client side\n");

	hh = (struct ipaccess_head *)msg->data;
	/* check if we have a route for this message. */
	if (bitvec_get_bit_pos(
	     &conn->route->shared->dst.streamid_map,
	     hh->proto) != ONE) {
		LOGP(DLINP, LOGL_NOTICE, "we don't have a "
			"route for streamid 0x%x\n", hh->proto);
		msgb_free(msg);
		return 0;
	}
	/* mangle message, if required. */
	hh->proto = conn->route->shared->src.streamid[hh->proto];

	ipa_server_conn_send(conn->src, msg);
	return 0;
}

static int ipa_sock_src_cb(struct ipa_server_conn *peer, struct msgb *msg)
{
	struct ipaccess_head *hh;
	struct ipa_proxy_conn *conn = peer->data;

	LOGP(DLINP, LOGL_NOTICE, "received message from server side\n");

	hh = (struct ipaccess_head *)msg->data;
	/* check if we have a route for this message. */
	if (bitvec_get_bit_pos(&conn->route->shared->src.streamid_map,
			hh->proto) != ONE) {
		LOGP(DLINP, LOGL_NOTICE, "we don't have a "
			"route for streamid 0x%x\n", hh->proto);
		msgb_free(msg);
		return 0;
	}
	/* mangle message, if required. */
	hh->proto = conn->route->shared->dst.streamid[hh->proto];

	ipa_client_conn_send(conn->dst, msg);
	return 0;
}

static int
ipa_sock_src_accept_cb(struct ipa_server_link *link, int fd)
{
	struct ipa_proxy_route *route = link->data;
	struct ipa_proxy_conn *conn;

	conn = talloc_zero(tall_ipa_proxy_ctx, struct ipa_proxy_conn);
	if (conn == NULL) {
		LOGP(DLINP, LOGL_ERROR, "cannot allocate memory for "
				       "origin IPA\n");
		close(fd);
		return -ENOMEM;
	}
	conn->route = route;

	conn->src = ipa_server_conn_create(tall_ipa_proxy_ctx, link, fd,
					   ipa_sock_src_cb, NULL, conn);
	if (conn->src == NULL) {
		LOGP(DLINP, LOGL_ERROR, "could not create server peer: %s\n",
			strerror(errno));
		return -ENOMEM;
	}

	LOGP(DLINP, LOGL_NOTICE, "now trying to connect to destination\n");

	conn->dst = ipa_client_conn_create(NULL, NULL, 0,
					   route->shared->dst.inst->net.addr,
					   route->shared->dst.inst->net.port,
					   NULL,
					   ipa_sock_dst_cb,
					   NULL,
					   conn);
	if (conn->dst == NULL) {
		LOGP(DLINP, LOGL_ERROR, "could not create client: %s\n",
			strerror(errno));
		return -ENOMEM;
	}
	if (ipa_client_conn_open(conn->dst) < 0) {
		LOGP(DLINP, LOGL_ERROR, "could not start client: %s\n",
			strerror(errno));
		return -ENOMEM;
	}
	llist_add(&conn->head, &route->shared->conn_list);
	return 0;
}

/*
 * VTY commands for IPA
 */
static int __ipa_instance_add(struct vty *vty, int argc, const char *argv[])
{
	struct ipa_proxy_instance *ipi;
	enum ipa_proxy_instance_net_type type;
	struct in_addr addr;
	uint16_t port;

	if (argc < 4)
		return CMD_ERR_INCOMPLETE;

	llist_for_each_entry(ipi, &ipa_instance_list, head) {
		if (strncmp(ipi->name, argv[0], IPA_INSTANCE_NAME) != 0)
			continue;

		vty_out(vty, "%% instance `%s' already exists%s",
			ipi->name, VTY_NEWLINE);
		return CMD_WARNING;
	}
	if (strncmp(argv[1], "bind", IPA_INSTANCE_NAME) == 0)
		type = IPA_INSTANCE_T_BIND;
	else if (strncmp(argv[1], "connect", IPA_INSTANCE_NAME) == 0)
		type = IPA_INSTANCE_T_CONNECT;
	else
		return CMD_ERR_INCOMPLETE;

	if (inet_aton(argv[2], &addr) < 0) {
		vty_out(vty, "%% invalid address %s%s", argv[1], VTY_NEWLINE);
		return CMD_WARNING;
	}
	port = atoi(argv[3]);

	ipi = talloc_zero(tall_ipa_proxy_ctx, struct ipa_proxy_instance);
	if (ipi == NULL) {
		vty_out(vty, "%% can't allocate memory for new instance%s",
			VTY_NEWLINE);
		return CMD_WARNING;
	}
	strncpy(ipi->name, argv[0], IPA_INSTANCE_NAME);
	ipi->name[IPA_INSTANCE_NAME - 1] = '\0';
	ipi->net.type = type;
	ipi->net.addr = talloc_strdup(tall_ipa_proxy_ctx, argv[2]);
	ipi->net.port = port;
	llist_add_tail(&ipi->head, &ipa_instance_list);

	return CMD_SUCCESS;
}

DEFUN(ipa_instance_add, ipa_instance_add_cmd,
      "ipa instance NAME (bind|connect) IP tcp port PORT",
      "Bind or connect instance to address and port")
{
	return __ipa_instance_add(vty, argc, argv);
}

DEFUN(ipa_instance_del, ipa_instance_del_cmd,
      "no ipa instance NAME",
      "Delete instance to address and port")
{
	struct ipa_proxy_instance *ipi;

	if (argc < 1)
		return CMD_ERR_INCOMPLETE;

	llist_for_each_entry(ipi, &ipa_instance_list, head) {
		if (strncmp(ipi->name, argv[0], IPA_INSTANCE_NAME) != 0)
			continue;

		if (ipi->refcnt > 0) {
			vty_out(vty, "%% instance `%s' is in use%s",
			ipi->name, VTY_NEWLINE);
			return CMD_WARNING;
		}
		llist_del(&ipi->head);
		talloc_free(ipi);
		return CMD_SUCCESS;
	}
	vty_out(vty, "%% instance `%s' does not exist%s",
		ipi->name, VTY_NEWLINE);

	return CMD_WARNING;
}

DEFUN(ipa_instance_show, ipa_instance_show_cmd,
      "ipa instance show", "Show existing ipaccess proxy instances")
{
	struct ipa_proxy_instance *this;

	llist_for_each_entry(this, &ipa_instance_list, head) {
		vty_out(vty, "instance %s %s %s tcp port %u%s",
			this->name, this->net.addr,
			this->net.type == IPA_INSTANCE_T_BIND ?
				"bind" : "connect",
			this->net.port, VTY_NEWLINE);
	}
	return CMD_SUCCESS;
}

static int __ipa_route_add(struct vty *vty, int argc, const char *argv[])
{
	struct ipa_proxy_instance *ipi = vty->index;
	struct ipa_proxy_instance *src = NULL, *dst = NULL;
	uint32_t src_streamid, dst_streamid;
	struct ipa_proxy_route *route, *matching_route = NULL;
	struct ipa_proxy_route_shared *shared = NULL;
	int ret;

	if (argc < 4)
		return CMD_ERR_INCOMPLETE;

	llist_for_each_entry(ipi, &ipa_instance_list, head) {
		if (strncmp(ipi->name, argv[0], IPA_INSTANCE_NAME) == 0) {
			src = ipi;
			continue;
		}
		if (strncmp(ipi->name, argv[2], IPA_INSTANCE_NAME) == 0) {
			dst = ipi;
			continue;
		}
	}
	if (src == NULL) {
		vty_out(vty, "%% instance `%s' does not exists%s",
			argv[0], VTY_NEWLINE);
		return CMD_WARNING;
	}
	if (dst == NULL) {
		vty_out(vty, "%% instance `%s' does not exists%s",
			argv[2], VTY_NEWLINE);
		return CMD_WARNING;
	}
	if (src->net.type != IPA_INSTANCE_T_BIND) {
		vty_out(vty, "%% instance `%s' is not of bind type%s",
			argv[0], VTY_NEWLINE);
		return CMD_WARNING;
	}
	if (dst->net.type != IPA_INSTANCE_T_CONNECT) {
		vty_out(vty, "%% instance `%s' is not of connect type%s",
			argv[2], VTY_NEWLINE);
		return CMD_WARNING;
	}
	src_streamid = strtoul(argv[1], NULL, 16);
	if (src_streamid > 0xff) {
		vty_out(vty, "%% source streamid must be "
			     ">= 0x00 and <= 0xff%s", VTY_NEWLINE);
		return CMD_WARNING;
	}
	dst_streamid = strtoul(argv[3], NULL, 16);
	if (dst_streamid > 0xff) {
		vty_out(vty, "%% destination streamid must be "
			     ">= 0x00 and <= 0xff%s", VTY_NEWLINE);
		return CMD_WARNING;
	}
	llist_for_each_entry(route, &ipa_proxy_route_list, head) {
		if (route->shared->src.inst == src &&
		    route->shared->dst.inst == dst) {
			if (route->src.streamid == src_streamid &&
			    route->dst.streamid == dst_streamid) {
				vty_out(vty, "%% this route already exists%s",
					VTY_NEWLINE);
				return CMD_WARNING;
			}
			matching_route = route;
			break;
		}
	}
	/* new route for this configuration. */
	route = talloc_zero(tall_ipa_proxy_ctx, struct ipa_proxy_route);
	if (route == NULL) {
		vty_out(vty, "%% can't allocate memory for new route%s",
			VTY_NEWLINE);
		return CMD_WARNING;
	}
	route->src.streamid = src_streamid;
	route->dst.streamid = dst_streamid;

	if (matching_route != NULL) {
		/* there's already a master route for these configuration. */
		if (matching_route->shared->src.inst != src) {
			vty_out(vty, "%% route does not contain "
				     "source instance `%s'%s",
				     argv[0], VTY_NEWLINE);
			return CMD_WARNING;
		}
		if (matching_route->shared->dst.inst != dst) {
			vty_out(vty, "%% route does not contain "
				     "destination instance `%s'%s",
				     argv[2], VTY_NEWLINE);
			return CMD_WARNING;
		}
		/* use already existing shared routing information. */
		shared = matching_route->shared;
	} else {
		struct ipa_server_link *link;

		/* this is a brand new route, allocate shared routing info. */
		shared = talloc_zero(tall_ipa_proxy_ctx, struct ipa_proxy_route_shared);
		if (shared == NULL) {
			vty_out(vty, "%% can't allocate memory for "
				"new route shared%s", VTY_NEWLINE);
			return CMD_WARNING;
		}
		shared->src.streamid_map.data_len =
			sizeof(shared->src.streamid_map_data);
		shared->src.streamid_map.data =
			shared->src.streamid_map_data;
		shared->dst.streamid_map.data_len =
			sizeof(shared->dst.streamid_map_data);
		shared->dst.streamid_map.data =
			shared->dst.streamid_map_data;

		link = ipa_server_link_create(tall_ipa_proxy_ctx, NULL,
						"0.0.0.0",
						src->net.port,
						ipa_sock_src_accept_cb, route);
		if (link == NULL) {
			vty_out(vty, "%% can't bind instance `%s' to port%s",
				src->name, VTY_NEWLINE);
			return CMD_WARNING;
		}
		if (ipa_server_link_open(link) < 0) {
			vty_out(vty, "%% can't bind instance `%s' to port%s",
				src->name, VTY_NEWLINE);
			return CMD_WARNING;
		}
		INIT_LLIST_HEAD(&shared->conn_list);
	}
	route->shared = shared;
	src->refcnt++;
	route->shared->src.inst = src;
	dst->refcnt++;
	route->shared->dst.inst = dst;
	shared->src.streamid[src_streamid] = dst_streamid;
	shared->dst.streamid[dst_streamid] = src_streamid;
	ret = bitvec_set_bit_pos(&shared->src.streamid_map, src_streamid, ONE);
	if (ret < 0) {
		vty_out(vty, "%% bad bitmask (?)%s", VTY_NEWLINE);
		return CMD_WARNING;
	}
	ret = bitvec_set_bit_pos(&shared->dst.streamid_map, dst_streamid, ONE);
	if (ret < 0) {
		vty_out(vty, "%% bad bitmask (?)%s", VTY_NEWLINE);
		return CMD_WARNING;
	}
	shared->refcnt++;

	llist_add_tail(&route->head, &ipa_proxy_route_list);
	return CMD_SUCCESS;
}

DEFUN(ipa_route_add, ipa_route_add_cmd,
      "ipa route instance NAME streamid HEXNUM "
		"instance NAME streamid HEXNUM", "Add IPA route")
{
	return __ipa_route_add(vty, argc, argv);
}

DEFUN(ipa_route_del, ipa_route_del_cmd,
      "no ipa route instance NAME streamid HEXNUM "
		   "instance NAME streamid HEXNUM", "Delete IPA route")
{
	struct ipa_proxy_instance *ipi = vty->index;
	struct ipa_proxy_instance *src = NULL, *dst = NULL;
	uint32_t src_streamid, dst_streamid;
	struct ipa_proxy_route *route, *matching_route = NULL;
	struct ipa_proxy_conn *conn, *tmp;

	if (argc < 4)
		return CMD_ERR_INCOMPLETE;

	llist_for_each_entry(ipi, &ipa_instance_list, head) {
		if (strncmp(ipi->name, argv[0], IPA_INSTANCE_NAME) == 0) {
			src = ipi;
			continue;
		}
		if (strncmp(ipi->name, argv[2], IPA_INSTANCE_NAME) == 0) {
			dst = ipi;
			continue;
		}
	}
	if (src == NULL) {
		vty_out(vty, "%% instance `%s' does not exists%s",
			argv[0], VTY_NEWLINE);
		return CMD_WARNING;
	}
	if (dst == NULL) {
		vty_out(vty, "%% instance `%s' does not exists%s",
			argv[2], VTY_NEWLINE);
		return CMD_WARNING;
	}
	if (src->net.type != IPA_INSTANCE_T_BIND) {
		vty_out(vty, "%% instance `%s' is not of bind type%s",
			argv[0], VTY_NEWLINE);
		return CMD_WARNING;
	}
	if (dst->net.type != IPA_INSTANCE_T_CONNECT) {
		vty_out(vty, "%% instance `%s' is not of connect type%s",
			argv[2], VTY_NEWLINE);
		return CMD_WARNING;
	}
	src_streamid = strtoul(argv[1], NULL, 16);
	if (src_streamid > 0xff) {
		vty_out(vty, "%% source streamid must be "
			     ">= 0x00 and <= 0xff%s", VTY_NEWLINE);
		return CMD_WARNING;
	}
	dst_streamid = strtoul(argv[3], NULL, 16);
	if (dst_streamid > 0xff) {
		vty_out(vty, "%% destination streamid must be "
			     ">= 0x00 and <= 0xff%s", VTY_NEWLINE);
		return CMD_WARNING;
	}
	llist_for_each_entry(route, &ipa_proxy_route_list, head) {
		if (route->shared->src.inst == src &&
		    route->shared->dst.inst == dst &&
		    route->src.streamid == src_streamid &&
		    route->dst.streamid == dst_streamid) {
			matching_route = route;
			break;
		}
	}
	if (matching_route == NULL) {
		vty_out(vty, "%% no route with that configuration%s",
			VTY_NEWLINE);
		return CMD_WARNING;
	}
	/* delete this route from list. */
	llist_del(&matching_route->head);

	if (--matching_route->shared->refcnt == 0) {
		/* nobody else using this route, release all resources. */
		llist_for_each_entry_safe(conn, tmp,
				&matching_route->shared->conn_list, head) {
			ipa_server_conn_destroy(conn->src);
			llist_del(&conn->head);
			talloc_free(conn);
		}
		osmo_fd_unregister(&route->shared->bfd);
		close(route->shared->bfd.fd);
		route->shared->bfd.fd = -1;

		talloc_free(route->shared);
	} else {
		/* otherwise, revert the mapping that this route applies. */
		bitvec_set_bit_pos(&matching_route->shared->src.streamid_map,
				   src_streamid, ZERO);
		bitvec_set_bit_pos(&matching_route->shared->dst.streamid_map,
				   dst_streamid, ZERO);
		matching_route->shared->src.streamid[src_streamid] = 0x00;
		matching_route->shared->dst.streamid[dst_streamid] = 0x00;
	}
	matching_route->shared->src.inst->refcnt--;
	matching_route->shared->dst.inst->refcnt--;
	talloc_free(matching_route);

	return CMD_SUCCESS;
}

DEFUN(ipa_route_show, ipa_route_show_cmd,
      "ipa route show", "Show existing ipaccess proxy routes")
{
	struct ipa_proxy_route *this;

	llist_for_each_entry(this, &ipa_proxy_route_list, head) {
		vty_out(vty, "route instance %s streamid 0x%.2x "
			           "instance %s streamid 0x%.2x%s",
			this->shared->src.inst->name, this->src.streamid,
			this->shared->dst.inst->name, this->dst.streamid,
			VTY_NEWLINE);
	}
	return CMD_SUCCESS;
}

/*
 * Config for ipaccess-proxy
 */
DEFUN(ipa_cfg, ipa_cfg_cmd, "ipa", "Configure the ipaccess proxy")
{
	vty->index = NULL;
	vty->node = L_IPA_NODE;
	return CMD_SUCCESS;
}

/* all these below look like enable commands, but without the ipa prefix. */
DEFUN(ipa_route_cfg_add, ipa_route_cfg_add_cmd,
      "route instance NAME streamid HEXNUM "
	    "instance NAME streamid HEXNUM", "Add IPA route")
{
	return __ipa_route_add(vty, argc, argv);
}

DEFUN(ipa_instance_cfg_add, ipa_instance_cfg_add_cmd,
      "instance NAME (bind|connect) IP tcp port PORT",
      "Bind or connect instance to address and port")
{
	return __ipa_instance_add(vty, argc, argv);
}

struct cmd_node ipa_node = {
	L_IPA_NODE,
	"%s(config-ipa)# ",
	1,
};

static int ipa_cfg_write(struct vty *vty)
{
	bool heading = false;
	struct ipa_proxy_instance *inst;
	struct ipa_proxy_route *route;

	llist_for_each_entry(inst, &ipa_instance_list, head) {
		if (!heading) {
			vty_out(vty, "ipa%s", VTY_NEWLINE);
			heading = true;
		}
		vty_out(vty, " instance %s %s %s tcp port %u%s",
			inst->name,
			inst->net.type == IPA_INSTANCE_T_BIND ?
				"bind" : "connect",
			inst->net.addr,
			inst->net.port, VTY_NEWLINE);
	}
	llist_for_each_entry(route, &ipa_proxy_route_list, head) {
		vty_out(vty, " route instance %s streamid 0x%.2x "
				    "instance %s streamid 0x%.2x%s",
			route->shared->src.inst->name, route->src.streamid,
			route->shared->dst.inst->name, route->dst.streamid,
			VTY_NEWLINE);
	}
	return CMD_SUCCESS;
}

void ipa_proxy_vty_init(void)
{
	tall_ipa_proxy_ctx =
		talloc_named_const(libosmo_abis_ctx, 1, "ipa_proxy");

	install_element(ENABLE_NODE, &ipa_instance_add_cmd);
	install_element(ENABLE_NODE, &ipa_instance_del_cmd);
	install_element(ENABLE_NODE, &ipa_instance_show_cmd);
	install_element(ENABLE_NODE, &ipa_route_add_cmd);
	install_element(ENABLE_NODE, &ipa_route_del_cmd);
	install_element(ENABLE_NODE, &ipa_route_show_cmd);

	install_element(CONFIG_NODE, &ipa_cfg_cmd);
	install_node(&ipa_node, ipa_cfg_write);
	install_element(L_IPA_NODE, &ipa_instance_cfg_add_cmd);
	install_element(L_IPA_NODE, &ipa_route_cfg_add_cmd);
}
