// SPDX-License-Identifier: GPL-2.0-or-later
/*
 * Copyright (C) 2022 Felix Fietkau <nbd@nbd.name>
 */
#include <libubox/avl-cmp.h>
#include "unetd.h"

enum {
	SERVICE_ATTR_TYPE,
	SERVICE_ATTR_CONFIG,
	SERVICE_ATTR_MEMBERS,
	__SERVICE_ATTR_MAX
};

static const struct blobmsg_policy service_policy[__SERVICE_ATTR_MAX] = {
	[SERVICE_ATTR_TYPE] = { "type", BLOBMSG_TYPE_STRING },
	[SERVICE_ATTR_CONFIG] = { "config", BLOBMSG_TYPE_TABLE },
	[SERVICE_ATTR_MEMBERS] = { "members", BLOBMSG_TYPE_ARRAY },
};

void network_services_free(struct network *net)
{
	vlist_flush_all(&net->services);
}

static int
__service_add_member(struct network_host **list, int *n, struct network_host *member)
{
	int i;

	for (i = 0; i < *n; i++) {
		if (list[i] == member)
			return 0;
	}

	list[(*n)++] = member;
	return 1;
}

static int
__service_add_group(struct network_host **list, int *n, struct network_group *group)
{
	int i, count = 0;

	for (i = 0; i < group->n_members; i++)
		count += __service_add_member(list, n, group->members[i]);

	return count;
}

static int
__service_parse_members(struct network *net, struct network_service *s,
			const char *name)
{
	struct network_group *group;
	struct network_host *host;

	if (name[0] != '@') {
		host = avl_find_element(&net->hosts, name, host, node);

		if (!host)
			return 0;

		if (s)
			__service_add_member(s->members, &s->n_members, host);

		return 1;
	}

	name++;
	group = avl_find_element(&net->groups, name, group, node);
	if (!group)
		return 0;

	if (s)
		return __service_add_group(s->members, &s->n_members, group);
	else
		return group->n_members;
}

static int
service_parse_members(struct network *net, struct network_service *s,
		      struct blob_attr *data)
{
	struct blob_attr *cur;
	int rem;
	int n = 0;

	blobmsg_for_each_attr(cur, data, rem)
		n += __service_parse_members(net, s, blobmsg_get_string(cur));

	return n;
}

static void
service_add(struct network *net, struct blob_attr *data)
{
	struct network_service *s;
	struct blob_attr *tb[__SERVICE_ATTR_MAX];
	struct blob_attr *cur, *config;
	const char *name = blobmsg_name(data);
	const char *type = NULL;
	char *name_buf, *type_buf;
	void *config_buf;
	int n_members;

	blobmsg_parse(service_policy, __SERVICE_ATTR_MAX, tb,
		      blobmsg_data(data), blobmsg_len(data));

	if ((cur = tb[SERVICE_ATTR_TYPE]) != NULL)
		type = blobmsg_get_string(cur);

	if (blobmsg_check_array(tb[SERVICE_ATTR_MEMBERS], BLOBMSG_TYPE_STRING) < 0)
		return;

	config = tb[SERVICE_ATTR_CONFIG];

	n_members = service_parse_members(net, NULL, tb[SERVICE_ATTR_MEMBERS]);
	s = calloc_a(sizeof(*s) + n_members * sizeof(s->members[0]),
		     &name_buf, strlen(name) + 1,
		     &type_buf, type ? strlen(type) + 1 : 0,
		     &config_buf, config ? blob_pad_len(config) : 0);

	strcpy(name_buf, name);
	if (type)
		s->type = strcpy(type_buf, type);
	if (config)
		s->config = memcpy(config_buf, config, blob_pad_len(config));
#ifdef VXLAN_SUPPORT
	if (type && !strcmp(type, "vxlan"))
		s->ops = &vxlan_ops;
#endif

	service_parse_members(net, s, tb[SERVICE_ATTR_MEMBERS]);
	vlist_add(&net->services, &s->node, name_buf);
}

void network_services_add(struct network *net, struct blob_attr *data)
{
	struct blob_attr *cur;
	int rem;

	blobmsg_for_each_attr(cur, data, rem)
		service_add(net, cur);
}

static void
service_update(struct vlist_tree *tree, struct vlist_node *node_new,
	       struct vlist_node *node_old)
{
	struct network *net = container_of(tree, struct network, services);
	struct network_service *s_old, *s_new;

	s_new = container_of_safe(node_new, struct network_service, node);
	s_old = container_of_safe(node_old, struct network_service, node);

	if (s_new && s_old && s_new->ops && s_new->ops == s_old->ops) {
		s_new->ops->init(net, s_new, s_old);
		goto out;
	}

	if (s_new && s_new->ops)
		s_new->ops->init(net, s_new, NULL);

	if (s_old && s_old->ops)
		s_old->ops->free(net, s_old);

out:
	free(s_old);
}

void network_services_peer_update(struct network *net, struct network_peer *peer)
{
	struct network_service *s;

	vlist_for_each_element(&net->services, s, node) {
		if (!s->ops || !s->ops->peer_update)
			continue;

		s->ops->peer_update(net, s, peer);
	}
}

void network_services_init(struct network *net)
{
	vlist_init(&net->services, avl_strcmp, service_update);
}
