/*
   Copyright (c) 2016, Matthias Schiffer <mschiffer@universe-factory.net>
   All rights reserved.

   Redistribution and use in source and binary forms, with or without
   modification, are permitted provided that the following conditions are met:

   1. Redistributions of source code must retain the above copyright notice,
   this list of conditions and the following disclaimer.
   2. Redistributions in binary form must reproduce the above copyright notice,
   this list of conditions and the following disclaimer in the documentation
   and/or other materials provided with the distribution.

   THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
   AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
   IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
   DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
   FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
   DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
   SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
   CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
   OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
   OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
   */


#include <respondd.h>

#include <json-c/json.h>
#include <libgluonutil.h>
#include <uci.h>

#include <inttypes.h>
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>

#include <arpa/inet.h>
#include <net/if.h>
#include <netinet/in.h>

#include <sys/types.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <ifaddrs.h>

#include <netdb.h>
#include <errno.h>
#include <libbabelhelper/babelhelper.h>

#include <libubox/blobmsg_json.h>
#include <libubus.h>

#define SOCKET_INPUT_BUFFER_SIZE 255

#define PROTOLEN 32

#define UBUS_TIMEOUT 30000

static struct babelhelper_ctx bhelper_ctx = {};

static bool get_linklocal_address(const char *ifname, char lladdr[INET6_ADDRSTRLEN]) {
	struct ifaddrs *ifaddr, *ifa;
	bool ret = false;

	if (getifaddrs(&ifaddr) == -1) {
		perror("getifaddrs");
		return false;
	}

	for (ifa = ifaddr; ifa != NULL; ifa = ifa->ifa_next) {
		if (!ifa->ifa_addr)
			continue;

		if (ifa->ifa_addr->sa_family != AF_INET6)
			continue;

		if (strcmp(ifname, ifa->ifa_name) != 0)
			continue;

		const struct in6_addr *address = &((const struct sockaddr_in6 *)ifa->ifa_addr)->sin6_addr;
		if (!IN6_IS_ADDR_LINKLOCAL(address))
			continue;

		if (!inet_ntop(AF_INET6, address, lladdr, INET6_ADDRSTRLEN)) {
			perror("inet_ntop");
			continue;
		}

		ret = true;
		break;
	}

	freeifaddrs(ifaddr);
	return ret;
}


static char*  get_line_from_run(const char* command) {
	FILE *fp;
	char *line = NULL;
	size_t len = 0;

	fp = popen(command, "r");

	if (fp != NULL) {
		ssize_t r = getline(&line, &len, fp);
		if (r >= 0) {
			len = strlen(line);

			if (len && line[len-1] == '\n')
				line[len-1] = 0;
		}
		else {
			free(line);
			line = NULL;
		}

		pclose(fp);
	}
	return line;
}

static struct json_object * get_addresses(void) {
	char *primarymac = gluonutil_get_sysconfig("primary_mac");
	char *address = malloc(INET6_ADDRSTRLEN+1);
	char node_prefix_str[INET6_ADDRSTRLEN+1];
	struct in6_addr node_prefix = {};
	struct json_object *retval = json_object_new_array();

	if (!gluonutil_get_node_prefix6(&node_prefix)) {
		fprintf(stderr, "get_addresses: could not obtain mesh-prefix from site.conf. Not adding addresses to json data\n");
		goto free;
	}

	if (inet_ntop(AF_INET6, &(node_prefix.s6_addr), node_prefix_str, INET6_ADDRSTRLEN) == NULL) {
		fprintf(stderr, "get_addresses: could not convert mesh-prefix from site.conf to string\n");
		goto free;
	}

	char *prefix_addresspart = strndup(node_prefix_str, INET6_ADDRSTRLEN);
	if (! babelhelper_generateip_str(address, primarymac, prefix_addresspart) ) {
		fprintf(stderr, "IP-address could not be generated by babelhelper");
	}
	free(prefix_addresspart);

	json_object_array_add(retval, json_object_new_string(address));

free:
	free(address);
	free(primarymac);

	return retval;
}

static void mesh_add_if(const char *ifname, struct json_object *wireless,
		struct json_object *tunnel, struct json_object *other) {
	char str_ip[INET6_ADDRSTRLEN];

	if (!get_linklocal_address(ifname, str_ip))
		return;

	struct json_object *address = json_object_new_string(str_ip);

	/* In case of VLAN and bridge interfaces, we want the lower interface
	 * to determine the interface type (but not for the interface address) */
	char lowername[IF_NAMESIZE];
	gluonutil_get_interface_lower(lowername, ifname);

	switch(gluonutil_get_interface_type(lowername)) {
	case GLUONUTIL_INTERFACE_TYPE_WIRELESS:
		json_object_array_add(wireless, address);
		break;

	case GLUONUTIL_INTERFACE_TYPE_TUNNEL:
		json_object_array_add(tunnel, address);
		break;

	default:
		json_object_array_add(other, address);
	}
}


static bool handle_neighbour(char **data, void *obj) {
	if (data[NEIGHBOUR]) {
		struct json_object *neigh = json_object_new_object();

		if (data[RXCOST])
			json_object_object_add(neigh, "rxcost", json_object_new_int(atoi(data[RXCOST])));
		if (data[TXCOST])
			json_object_object_add(neigh, "txcost", json_object_new_int(atoi(data[TXCOST])));
		if (data[COST])
			json_object_object_add(neigh, "cost", json_object_new_int(atoi(data[COST])));
		if (data[REACH])
			json_object_object_add(neigh, "reachability", json_object_new_double(strtod(data[REACH], NULL)));

		if (!data[IF])
			return true;

		struct json_object *nif;
		if (!json_object_object_get_ex(obj, data[IF], &nif)) {
			char str_ip[INET6_ADDRSTRLEN];

			nif = json_object_new_object();

			if (get_linklocal_address(data[IF], str_ip))
				json_object_object_add(nif, "ll-addr", json_object_new_string(str_ip));

			json_object_object_add(nif, "protocol", json_object_new_string("babel"));
			json_object_object_add(obj, data[IF], nif);

			json_object_object_add(nif, "neighbours", json_object_new_object());
		}

		struct json_object *neighborcollector;
		json_object_object_get_ex(nif, "neighbours", &neighborcollector);
		json_object_object_add(neighborcollector, data[ADDRESS], neigh);

	}
	return true;
}

static struct json_object * get_babel_neighbours(void) {

	struct json_object *neighbours;
	neighbours  = json_object_new_object();
	if (!neighbours)
		return NULL;

	babelhelper_readbabeldata(&bhelper_ctx, "dump", (void*)neighbours, handle_neighbour);

	return(neighbours);
}

static void blobmsg_handle_list(struct blob_attr *attr, int len, bool array, struct json_object *wireless, struct json_object *tunnel, struct json_object *other);

static void blobmsg_handle_element(struct blob_attr *attr, bool head, char **ifname, char **proto, struct json_object *wireless, struct json_object *tunnel, struct json_object *other) {
	void *data;

	if (!blobmsg_check_attr(attr, false))
		return;

	data = blobmsg_data(attr);

	switch (blob_id(attr)) {
		case  BLOBMSG_TYPE_STRING:
			if (!strncmp(blobmsg_name(attr), "device", 6)) {
				free(*ifname);
				*ifname = strndup(data, IF_NAMESIZE);
			} else if (!strncmp(blobmsg_name(attr), "proto", 5)) {
				free(*proto);
				*proto = strndup(data, PROTOLEN);
			}
			return;
		case BLOBMSG_TYPE_ARRAY:
			blobmsg_handle_list(data, blobmsg_data_len(attr), true, wireless, tunnel, other);
			return;
		case BLOBMSG_TYPE_TABLE:
			blobmsg_handle_list(data, blobmsg_data_len(attr), false, wireless, tunnel, other);
	}
}

static void blobmsg_handle_list(struct blob_attr *attr, int len, bool array, struct json_object *wireless, struct json_object *tunnel, struct json_object *other) {
	struct blob_attr *pos;
	int rem = len;

	char *ifname = NULL;
	char *proto = NULL;

	__blob_for_each_attr(pos, attr, rem) {
		blobmsg_handle_element(pos, array, &ifname, &proto, wireless, tunnel, other);
	}

	if (ifname && proto) {
		if (!strncmp(proto, "gluon_mesh", 10)) {
			mesh_add_if(ifname, wireless, tunnel, other);
		}
	}
	free(ifname);
	free(proto);
}

static void add_if_not_empty(struct json_object *obj, const char *key, struct json_object *val) {
	if (json_object_array_length(val))
		json_object_object_add(obj, key, val);
	else
		json_object_put(val);
}

static void receive_call_result_data(struct ubus_request *req, int type, struct blob_attr *msg) {
	struct json_object *ret = json_object_new_object();
	struct json_object *wireless = json_object_new_array();
	struct json_object *tunnel = json_object_new_array();
	struct json_object *other = json_object_new_array();

	if (!ret || !wireless || !tunnel || !other) {
		json_object_put(wireless);
		json_object_put(tunnel);
		json_object_put(other);
		json_object_put(ret);
		return;
	}

	if (!msg) {
		printf("empty message\n");
		return;
	}

	blobmsg_handle_list(blobmsg_data(msg), blobmsg_data_len(msg), false, wireless, tunnel, other);

	add_if_not_empty(ret, "wireless", wireless);
	add_if_not_empty(ret, "tunnel", tunnel);
	add_if_not_empty(ret, "other", other);

	*((struct json_object**)(req->priv)) = ret;
}


static struct json_object * get_mesh_ifs() {
	struct ubus_context *ubus_ctx;
	struct json_object *ret = NULL;
	struct blob_buf b = {};

	unsigned int id=8;

	ubus_ctx = ubus_connect(NULL);
	if (!ubus_ctx) {
		fprintf(stderr,"could not connect to ubus, not providing mesh-data\n");
		goto end;
	}

	blob_buf_init(&b, 0);
	ubus_lookup_id(ubus_ctx, "network.interface", &id);
	int uret = ubus_invoke(ubus_ctx, id, "dump", b.head, receive_call_result_data, &ret, UBUS_TIMEOUT);

	if (uret > 0)
		fprintf(stderr, "ubus command failed: %s\n", ubus_strerror(uret));
	else if (uret == -2)
		fprintf(stderr, "invalid call, exiting\n");

	blob_buf_free(&b);

end:
	ubus_free(ubus_ctx);
	return ret;
}

static struct json_object * get_mesh(void) {
	struct json_object *ret = json_object_new_object();
	struct json_object *interfaces = NULL;
	interfaces = json_object_new_object();
	json_object_object_add(interfaces, "interfaces", get_mesh_ifs());
	json_object_object_add(ret, "babel", interfaces);
	return ret;
}

static struct json_object * get_babeld_version(void) {
	char *version = get_line_from_run("exec babeld -V 2>&1");
	struct json_object *ret = gluonutil_wrap_string(version);
	free(version);
	return ret;
}

static struct json_object * respondd_provider_nodeinfo(void) {
	bhelper_ctx.debug=false;
	struct json_object *ret = json_object_new_object();

	struct json_object *network = json_object_new_object();
	json_object_object_add(network, "addresses", get_addresses());
	json_object_object_add(network, "mesh", get_mesh());
	json_object_object_add(ret, "network", network);

	struct json_object *software = json_object_new_object();
	struct json_object *software_babeld = json_object_new_object();
	json_object_object_add(software_babeld, "version", get_babeld_version());
	json_object_object_add(software, "babeld", software_babeld);
	json_object_object_add(ret, "software", software);

	return ret;
}

static struct json_object * read_number(const char *ifname, const char *stat) {
	const char *format = "/sys/class/net/%s/statistics/%s";

	struct json_object *ret = NULL;
	int64_t i;

	char path[strlen(format) + strlen(ifname) + strlen(stat) + 1];
	snprintf(path, sizeof(path), format, ifname, stat);

	FILE *f = fopen(path, "r");
	if (!f)
		return NULL;

	if (fscanf(f, "%"SCNd64, &i) == 1)
		ret = json_object_new_int64(i);

	fclose(f);

	return ret;
}

static struct json_object * get_traffic(void) {
	const char *ifname = "br-client";

	struct json_object *ret = NULL;
	struct json_object *rx = json_object_new_object();
	struct json_object *tx = json_object_new_object();

	json_object_object_add(rx, "packets", read_number(ifname, "rx_packets"));
	json_object_object_add(rx, "bytes", read_number(ifname, "rx_bytes"));
	json_object_object_add(rx, "dropped", read_number(ifname, "rx_dropped"));
	json_object_object_add(tx, "packets", read_number(ifname, "tx_packets"));
	json_object_object_add(tx, "dropped", read_number(ifname, "tx_dropped"));
	json_object_object_add(tx, "bytes", read_number(ifname, "tx_bytes"));

	ret = json_object_new_object();
	json_object_object_add(ret, "rx", rx);
	json_object_object_add(ret, "tx", tx);

	return ret;
}

static bool handle_route_addgw_nexthop(char **data, void *arg) {
	struct json_object *obj = (struct json_object*) arg;
	if (data[PREFIX] && data[FROM] && data[VIA] && data[IF]) {
		if ( (! strncmp(data[PREFIX], "::/0", 4) ) && ( ! strncmp(data[FROM], "::/0", 4) ) ) {
			int gw_nexthoplen=strlen(data[VIA]) + strlen(data[IF])+2;
			char gw_nexthop[gw_nexthoplen];
			snprintf(gw_nexthop, gw_nexthoplen , "%s%%%s", data[VIA], data[IF]);
			json_object_object_add(obj, "gateway_nexthop", json_object_new_string(gw_nexthop));
		}
	}
	return true;
}

static int json_parse_get_clients(json_object * object) {
	if (object) {
		json_object_object_foreach(object, key, val) {
			if (! strncmp("clients", key, 7)) {
				return(json_object_get_int(val));
			}
		}
	}
	return(-1);
}

static int ask_l3roamd_for_client_count() {
	struct sockaddr_un addr;
	const char *socket_path = "/var/run/l3roamd.sock";
	int fd;
	int clients = -1;
	char *buf = NULL;
	int already_read = 0;

	if ((fd = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) {
		fprintf(stderr, "could not setup l3roamd-control-socket\n");
		return(-1);
	}

	memset(&addr, 0, sizeof(addr));
	addr.sun_family = AF_UNIX;
	strncpy(addr.sun_path, socket_path, sizeof(addr.sun_path)-1);

	if (connect(fd, (struct sockaddr*)&addr, sizeof(addr)) == -1) {
		fprintf(stderr, "connect error\n");
		return(-1);
	}

	if (write(fd,"get_clients\n",12) != 12) {
		perror("could not send command to l3roamd socket: get_clients");
		goto end;
	}

	int rc = 0;
	do {
		char *buf_tmp = realloc(buf, already_read + SOCKET_INPUT_BUFFER_SIZE + 1);
		if (buf_tmp == NULL) {
			fprintf(stderr, "could not allocate memory for buffer\n");
			goto end;
		}
		buf = buf_tmp;

		rc = read(fd, &buf[already_read], SOCKET_INPUT_BUFFER_SIZE);
		already_read+=rc;
		if (rc < 0) {
			perror("error on read in ask_l3roamd_for_client_count():");
			goto end;
		}
		buf[already_read]='\0';
	} while (rc == SOCKET_INPUT_BUFFER_SIZE);

	json_object * jobj = json_tokener_parse(buf);
	clients = json_parse_get_clients(jobj);
	json_object_put(jobj);

end:
	free(buf);
	close(fd);

	return clients;
}

static struct json_object * get_clients(void) {
	struct json_object *ret = json_object_new_object();

	int total = ask_l3roamd_for_client_count();
	if (total >= 0)
		json_object_object_add(ret, "total", json_object_new_int(total));

	return ret;
}

static struct json_object * respondd_provider_statistics(void) {
	struct json_object *ret = json_object_new_object();

	json_object_object_add(ret, "clients", get_clients());
	json_object_object_add(ret, "traffic", get_traffic());

	babelhelper_readbabeldata(&bhelper_ctx, "dump", (void*)ret, handle_route_addgw_nexthop );

	return ret;
}

static struct json_object * respondd_provider_neighbours(void) {
	struct json_object *ret = json_object_new_object();

	struct json_object *babel = get_babel_neighbours();
	if (babel)
		json_object_object_add(ret, "babel", babel);


	return ret;
}


const struct respondd_provider_info respondd_providers[] = {
	{"nodeinfo", respondd_provider_nodeinfo},
	{"statistics", respondd_provider_statistics},
	{"neighbours", respondd_provider_neighbours},
	{}
};
