/*
 * vdev_globals_json.c: a virtual device manager for *nix
 * Copyright (C) 2026  Aitor <aitor@genuen.org>
 *
 * This program is dual-licensed: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 3 or later as
 * published by the Free Software Foundation. For the terms of this
 * license, see LICENSE.GPLv3+ or <http://www.gnu.org/licenses/>.
 *
 * You are free to use this program under the terms of the GNU General
 * Public License, 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.
 *
 * Alternatively, you are free to use this program under the terms of the
 * Internet Software Consortium License, but WITHOUT ANY WARRANTY; without
 * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
 * PURPOSE. For the terms of this license, see LICENSE.ISC or
 * <http://www.isc.org/downloads/software-support-policy/isc-license/>.
 */

#include "vdev_globals_json.h"

#include <ctype.h>
#include <stdbool.h>
#include <sys/stat.h>

static int vdev_globals_set_value(char **dst, char const *src)
{
	char *dup = vdev_strdup_or_null(src);

	if (dup == NULL)
		return -ENOMEM;
	if (*dst != NULL)
		free(*dst);
	*dst = dup;
	return 0;
}

static void vdev_action_allowlist_free(struct vdev_action_allowlist *al)
{
	size_t i;

	if (al == NULL)
		return;
	free(al->config_basename);
	al->config_basename = NULL;
	if (al->names != NULL) {
		for (i = 0; i < al->num_names; i++)
			free(al->names[i]);
		free(al->names);
		al->names = NULL;
	}
	al->num_names = 0;
	al->wildcard = false;
}

static void vdev_globals_allowlists_free(struct vdev_globals *g)
{
	size_t i;

	if (g == NULL || g->allowlists == NULL) {
		if (g != NULL) {
			g->allowlists = NULL;
			g->num_allowlists = 0;
		}
		return;
	}
	for (i = 0; i < g->num_allowlists; i++)
		vdev_action_allowlist_free(&g->allowlists[i]);
	free(g->allowlists);
	g->allowlists = NULL;
	g->num_allowlists = 0;
}

void vdev_globals_free(struct vdev_state *vdev)
{
	vdev_globals_allowlists_free(&vdev->globals);
	free(vdev->globals.sysfs);
	free(vdev->globals.config_dir);
	free(vdev->globals.long_running);
	memset(&vdev->globals, 0, sizeof(vdev->globals));
}

int vdev_globals_set_defaults(struct vdev_state *vdev)
{
	int rc = 0;

	rc = vdev_globals_set_value(&vdev->globals.sysfs, "/sys");
	if (rc != 0)
		return rc;
	rc = vdev_globals_set_value(&vdev->globals.config_dir, "/etc/vdev");
	if (rc != 0)
		return rc;
	rc = vdev_globals_set_value(&vdev->globals.long_running, "vdevd.conf");
	if (rc != 0)
		return rc;
	return 0;
}

static char *vdev_skip_ws(char *s)
{
	while (*s != '\0' && isspace((unsigned char)*s))
		s++;
	return s;
}

static char *vdev_skip_ws_bounded(char *s, char *end)
{
	while (s < end && *s != '\0' && isspace((unsigned char)*s))
		s++;
	return s;
}

/*
 * Strip //-to-EOL and slash-star block comments outside JSON strings (see
 * example/README.md “Comments in vdevd.globals.json”). Multi-line block
 * comments are handled; // inside "..." strings is not treated as a comment.
 */
static void vdev_globals_json_strip_comments(char *buf)
{
	char *dst = buf;
	char *src = buf;
	bool in_string = false;
	bool esc = false;
	bool line_comment = false;
	bool block_comment = false;

	for (; *src != '\0';) {
		if (line_comment) {
			if (*src == '\n') {
				*dst++ = '\n';
				line_comment = false;
			}
			src++;
			continue;
		}
		if (block_comment) {
			if (*src == '*' && src[1] == '/') {
				src += 2;
				block_comment = false;
			} else
				src++;
			continue;
		}
		if (in_string) {
			*dst++ = *src;
			if (esc) {
				esc = false;
			} else if (*src == '\\') {
				esc = true;
			} else if (*src == '"') {
				in_string = false;
			}
			src++;
			continue;
		}
		if (*src == '"') {
			in_string = true;
			esc = false;
			*dst++ = *src++;
			continue;
		}
		if (*src == '/' && src[1] == '/') {
			src += 2;
			line_comment = true;
			continue;
		}
		if (*src == '/' && src[1] == '*') {
			src += 2;
			block_comment = true;
			continue;
		}
		*dst++ = *src++;
	}
	*dst = '\0';
}

static int vdev_json_get_string_bounded(char *start, char *end, char const *key,
					char **out)
{
	char pat[128];
	char *p = NULL;
	char *q = NULL;
	char *e = NULL;
	char *dup = NULL;
	char *limit = end;
	size_t n = 0;
	size_t plen;

	if (start == NULL || key == NULL || out == NULL)
		return -EINVAL;
	if (limit == NULL)
		limit = start + strlen(start);

	snprintf(pat, sizeof(pat), "\"%s\"", key);
	plen = strlen(pat);
	p = start;
	while (p + plen <= limit) {
		if (memcmp(p, pat, plen) == 0)
			break;
		p++;
	}
	if (p + plen > limit)
		return -ENOENT;
	p += plen;
	p = vdev_skip_ws_bounded(p, limit);
	if (p >= limit || *p != ':')
		return -EINVAL;
	p++;
	p = vdev_skip_ws_bounded(p, limit);
	if (p >= limit || *p != '"')
		return -EINVAL;
	p++;
	q = p;
	while (q < limit && *q != '\0' && *q != '"')
		q++;
	if (q >= limit || *q != '"')
		return -EINVAL;
	n = (size_t)(q - p);
	dup = VDEV_CALLOC(char, n + 1);
	if (dup == NULL)
		return -ENOMEM;
	memcpy(dup, p, n);
	dup[n] = '\0';
	e = dup;
	while (*e != '\0') {
		if ((unsigned char)*e < 0x20) {
			free(dup);
			return -EINVAL;
		}
		e++;
	}
	*out = dup;
	return 0;
}

static int vdev_json_get_string(char *json, char const *key, char **out)
{
	return vdev_json_get_string_bounded(json, NULL, key, out);
}

static char *vdev_json_find_object(char *json, char const *name,
				   char **obj_end)
{
	char pat[128];
	char *p = NULL;
	char *q = NULL;
	int depth = 0;
	int in_str = 0;
	int esc = 0;

	snprintf(pat, sizeof(pat), "\"%s\"", name);
	p = strstr(json, pat);
	if (p == NULL)
		return NULL;
	p += strlen(pat);
	p = vdev_skip_ws(p);
	if (*p != ':')
		return NULL;
	p++;
	p = vdev_skip_ws(p);
	if (*p != '{')
		return NULL;

	q = p;
	while (*q != '\0') {
		char c = *q;
		if (in_str) {
			if (esc)
				esc = 0;
			else if (c == '\\')
				esc = 1;
			else if (c == '"')
				in_str = 0;
		} else if (c == '"') {
			in_str = 1;
		} else if (c == '{') {
			depth++;
		} else if (c == '}') {
			depth--;
			if (depth == 0) {
				if (obj_end != NULL)
					*obj_end = q;
				return p;
			}
		}
		q++;
	}
	return NULL;
}

/* Locate "name": [ ... ] within [start, end); return pointer to '[' or NULL. */
static char *vdev_json_find_array_bounded(char *start, char *end,
					  char const *name, char **arr_end)
{
	char pat[128];
	char *p = NULL;
	char *q = NULL;
	char *limit = end;
	size_t plen;
	int depth = 0;
	int in_str = 0;
	int esc = 0;

	if (start == NULL || name == NULL)
		return NULL;
	if (limit == NULL)
		limit = start + strlen(start);

	snprintf(pat, sizeof(pat), "\"%s\"", name);
	plen = strlen(pat);
	p = start;
	while (p + plen <= limit) {
		if (memcmp(p, pat, plen) == 0)
			break;
		p++;
	}
	if (p + plen > limit)
		return NULL;
	p += plen;
	p = vdev_skip_ws_bounded(p, limit);
	if (p >= limit || *p != ':')
		return NULL;
	p++;
	p = vdev_skip_ws_bounded(p, limit);
	if (p >= limit || *p != '[')
		return NULL;

	q = p;
	while (q < limit && *q != '\0') {
		char c = *q;
		if (in_str) {
			if (esc)
				esc = 0;
			else if (c == '\\')
				esc = 1;
			else if (c == '"')
				in_str = 0;
		} else if (c == '"') {
			in_str = 1;
		} else if (c == '[') {
			depth++;
		} else if (c == ']') {
			depth--;
			if (depth == 0) {
				if (arr_end != NULL)
					*arr_end = q;
				return p;
			}
		}
		q++;
	}
	return NULL;
}

static int vdev_json_parse_string_array(char *arr, char *arr_end,
					char ***out_names, size_t *out_n,
					bool *out_wildcard)
{
	char *p = arr;
	char **names = NULL;
	size_t n = 0;
	size_t cap = 0;

	*out_names = NULL;
	*out_n = 0;
	*out_wildcard = false;

	if (p == NULL || arr_end == NULL || *p != '[')
		return -EINVAL;
	p++;

	while (p < arr_end) {
		p = vdev_skip_ws_bounded(p, arr_end);
		if (p >= arr_end)
			break;
		if (*p == ']')
			break;
		if (*p == ',') {
			p++;
			continue;
		}
		if (*p != '"')
			return -EINVAL;
		p++;
		{
			char *q = p;
			char *dup;
			size_t len;

			while (q < arr_end && *q != '\0' && *q != '"')
				q++;
			if (q >= arr_end || *q != '"')
				return -EINVAL;
			len = (size_t)(q - p);
			dup = VDEV_CALLOC(char, len + 1);
			if (dup == NULL) {
				size_t i;
				for (i = 0; i < n; i++)
					free(names[i]);
				free(names);
				return -ENOMEM;
			}
			memcpy(dup, p, len);
			dup[len] = '\0';
			if (strcmp(dup, "*") == 0) {
				free(dup);
				*out_wildcard = true;
				/* keep scanning so syntax is validated */
			} else {
				if (n + 1 > cap) {
					size_t ncap = cap ? cap * 2 : 8;
					char **nn = realloc(names,
							    ncap * sizeof(char *));
					if (nn == NULL) {
						size_t i;
						free(dup);
						for (i = 0; i < n; i++)
							free(names[i]);
						free(names);
						return -ENOMEM;
					}
					names = nn;
					cap = ncap;
				}
				names[n++] = dup;
			}
			p = q + 1;
		}
	}

	if (*out_wildcard) {
		size_t i;
		for (i = 0; i < n; i++)
			free(names[i]);
		free(names);
		*out_names = NULL;
		*out_n = 0;
		return 0;
	}

	*out_names = names;
	*out_n = n;
	return 0;
}

static int vdev_globals_allowlist_add(struct vdev_globals *g,
				      char const *config_basename,
				      bool wildcard,
				      char **names, size_t num_names)
{
	struct vdev_action_allowlist *nal;
	char *cfg_dup;

	if (g == NULL || config_basename == NULL || config_basename[0] == '\0')
		return -EINVAL;

	cfg_dup = vdev_strdup_or_null(config_basename);
	if (cfg_dup == NULL)
		return -ENOMEM;

	nal = realloc(g->allowlists,
		      (g->num_allowlists + 1) * sizeof(*nal));
	if (nal == NULL) {
		free(cfg_dup);
		return -ENOMEM;
	}
	g->allowlists = nal;
	memset(&g->allowlists[g->num_allowlists], 0,
	       sizeof(g->allowlists[g->num_allowlists]));
	g->allowlists[g->num_allowlists].config_basename = cfg_dup;
	g->allowlists[g->num_allowlists].wildcard = wildcard;
	g->allowlists[g->num_allowlists].names = names;
	g->allowlists[g->num_allowlists].num_names = num_names;
	g->num_allowlists++;
	return 0;
}

/* Parse profiles.<key>.actions → allowlist keyed by that profile's config basename. */
static int vdev_globals_state_load_actions(struct vdev_globals *g,
					   char *states_obj, char const *key)
{
	char *inner_end = NULL;
	char *inner = NULL;
	char *cfg = NULL;
	char *arr_end = NULL;
	char *arr = NULL;
	char *act_str = NULL;
	char **names = NULL;
	size_t num_names = 0;
	bool wildcard = false;
	int rc;

	inner = vdev_json_find_object(states_obj, key, &inner_end);
	if (inner == NULL)
		return 0; /* state absent → no allowlist entry */

	rc = vdev_json_get_string_bounded(inner, inner_end, "config", &cfg);
	if (rc == -ENOENT)
		return 0;
	if (rc != 0)
		return rc;

	/* Prefer array form: "actions": [ "a.act", ... ] or [ "*" ] */
	arr = vdev_json_find_array_bounded(inner, inner_end, "actions", &arr_end);
	if (arr != NULL) {
		rc = vdev_json_parse_string_array(arr, arr_end, &names, &num_names,
						  &wildcard);
		if (rc != 0) {
			free(cfg);
			return rc;
		}
		rc = vdev_globals_allowlist_add(g, cfg, wildcard, names, num_names);
		if (rc != 0) {
			size_t i;
			for (i = 0; i < num_names; i++)
				free(names[i]);
			free(names);
			free(cfg);
			return rc;
		}
		free(cfg);
		return 0;
	}

	/* String form: "actions": "*" or "actions": "foo.act" */
	rc = vdev_json_get_string_bounded(inner, inner_end, "actions", &act_str);
	if (rc == -ENOENT) {
		/* no actions field → compat (no entry → load all) */
		free(cfg);
		return 0;
	}
	if (rc != 0) {
		free(cfg);
		return rc;
	}

	if (strcmp(act_str, "*") == 0) {
		free(act_str);
		rc = vdev_globals_allowlist_add(g, cfg, true, NULL, 0);
		free(cfg);
		return rc;
	}

	names = VDEV_CALLOC(char *, 1);
	if (names == NULL) {
		free(act_str);
		free(cfg);
		return -ENOMEM;
	}
	names[0] = act_str;
	rc = vdev_globals_allowlist_add(g, cfg, false, names, 1);
	if (rc != 0) {
		free(names[0]);
		free(names);
		free(cfg);
		return rc;
	}
	free(cfg);
	return 0;
}

/* One entry under "profiles": string (config basename) or object with "config". */
static int vdev_globals_state_load(char *states_obj, char const *key, char **cfg_dst)
{
	char *v = NULL;
	char *inner_end = NULL;
	char *inner = NULL;
	int rc;

	if (cfg_dst == NULL)
		return -EINVAL;

	rc = vdev_json_get_string(states_obj, key, &v);
	if (rc == 0) {
		rc = vdev_globals_set_value(cfg_dst, v);
		free(v);
		return rc;
	}
	free(v);
	v = NULL;
	if (rc != -ENOENT && rc != -EINVAL)
		return rc;

	inner = vdev_json_find_object(states_obj, key, &inner_end);
	if (inner == NULL)
		return -ENOENT;

	rc = vdev_json_get_string_bounded(inner, inner_end, "config", &v);
	if (rc == 0) {
		rc = vdev_globals_set_value(cfg_dst, v);
		free(v);
		return rc;
	}
	free(v);
	return rc;
}

static int vdev_action_name_matches(char const *listed, char const *basename)
{
	size_t llen;
	size_t blen;

	if (listed == NULL || basename == NULL)
		return 0;
	if (strcmp(listed, basename) == 0)
		return 1;

	llen = strlen(listed);
	blen = strlen(basename);

	/* listed "input" matches "input.act" */
	if (blen > 4 && strcmp(basename + blen - 4, ".act") == 0 &&
	    llen == blen - 4 && strncmp(listed, basename, llen) == 0)
		return 1;

	/* listed "input.act" matches basename already checked; also accept
	 * listed with .act when basename lacks it (defensive). */
	if (llen > 4 && strcmp(listed + llen - 4, ".act") == 0 &&
	    blen == llen - 4 && strncmp(listed, basename, blen) == 0)
		return 1;

	return 0;
}

int vdev_globals_action_allowed(struct vdev_globals const *globals,
				char const *config_path,
				char const *act_basename)
{
	char *cfg_base = NULL;
	size_t i, j;
	struct vdev_action_allowlist const *al = NULL;

	if (act_basename == NULL || act_basename[0] == '\0')
		return 0;
	if (globals == NULL || globals->num_allowlists == 0)
		return 1; /* no JSON filters → load all */

	cfg_base = vdev_basename(config_path, NULL);
	if (cfg_base == NULL)
		return 1; /* no config path → cannot key; allow */

	for (i = 0; i < globals->num_allowlists; i++) {
		if (globals->allowlists[i].config_basename != NULL &&
		    strcmp(globals->allowlists[i].config_basename, cfg_base) == 0) {
			al = &globals->allowlists[i];
			break;
		}
	}
	free(cfg_base);

	if (al == NULL)
		return 1; /* this profile has no actions field → allow all */
	if (al->wildcard)
		return 1;
	for (j = 0; j < al->num_names; j++) {
		if (vdev_action_name_matches(al->names[j], act_basename))
			return 1;
	}
	return 0;
}

int vdev_globals_load_json(struct vdev_state *vdev, char const *config_path)
{
	struct stat sb;
	char *cfgdir = NULL;
	char *json_path = NULL;
	char *json = NULL;
	FILE *f = NULL;
	int rc = 0;
	long nr = 0;
	char *v = NULL;
	static char const *const state_keys[] = {
		"initrd", "early", "terminal", "long-running", NULL
	};

	cfgdir = vdev_dirname(config_path, NULL);
	if (cfgdir == NULL)
		return -ENOMEM;
	json_path = vdev_fullpath(cfgdir, "vdevd.globals.json", NULL);
	if (json_path == NULL)
		goto _out;

	rc = stat(json_path, &sb);
	if (rc != 0) {
		rc = 0;
		goto _out;
	}
	if (!S_ISREG(sb.st_mode) || sb.st_size <= 0 || sb.st_size > 128 * 1024) {
		rc = -EINVAL;
		goto _out;
	}

	f = fopen(json_path, "rb");
	if (f == NULL) {
		rc = -errno;
		goto _out;
	}
	json = VDEV_CALLOC(char, (size_t)sb.st_size + 1);
	if (json == NULL) {
		rc = -ENOMEM;
		goto _out;
	}
	nr = (long)fread(json, 1, (size_t)sb.st_size, f);
	fclose(f);
	f = NULL;
	if ((size_t)nr != (size_t)sb.st_size) {
		rc = -EIO;
		goto _out;
	}
	json[(size_t)sb.st_size] = '\0';
	vdev_globals_json_strip_comments(json);

	/* Replace allowlists from this file (reload-safe). */
	vdev_globals_allowlists_free(&vdev->globals);

	rc = vdev_globals_set_value(&vdev->globals.config_dir, cfgdir);
	if (rc != 0)
		goto _out;

	/* top-level config-dir (back-compat), overridden by globals.config-dir */
	rc = vdev_json_get_string(json, "config-dir", &v);
	if (rc == 0) {
		rc = vdev_globals_set_value(&vdev->globals.config_dir, v);
		free(v);
		v = NULL;
		if (rc != 0)
			goto _out;
	} else if (rc != -ENOENT) {
		goto _out;
	}

	/* ".globals" object (preferred) */
	{
		char *g_end = NULL;
		char *g_obj = vdev_json_find_object(json, "globals", &g_end);

		if (g_obj != NULL) {
			int grc = vdev_json_get_string_bounded(g_obj, g_end,
							       "sysfs", &v);

			if (grc == 0) {
				rc = vdev_globals_set_value(&vdev->globals.sysfs, v);
				free(v);
				v = NULL;
				if (rc != 0)
					goto _out;
			} else if (grc != -ENOENT) {
				rc = grc;
				goto _out;
			}
			grc = vdev_json_get_string_bounded(g_obj, g_end,
							   "config-dir", &v);
			if (grc == 0) {
				rc = vdev_globals_set_value(&vdev->globals.config_dir, v);
				free(v);
				v = NULL;
				if (rc != 0)
					goto _out;
			} else if (grc != -ENOENT) {
				rc = grc;
				goto _out;
			}
		}
	}

	/* legacy top-level sysfs */
	rc = vdev_json_get_string(json, "sysfs", &v);
	if (rc == 0) {
		rc = vdev_globals_set_value(&vdev->globals.sysfs, v);
		free(v);
		v = NULL;
		if (rc != 0)
			goto _out;
	} else if (rc != -ENOENT) {
		goto _out;
	}

	/* profiles: long-running basename + per-profile actions allowlists.
	 * Legacy top-level key "states" is still accepted. */
	{
		char *profiles_end = NULL;
		char *profiles_obj = vdev_json_find_object(json, "profiles",
							     &profiles_end);
		int arc = 0;
		size_t si;

		if (profiles_obj == NULL)
			profiles_obj = vdev_json_find_object(json, "states",
							     &profiles_end);

		if (profiles_obj != NULL) {
			arc = vdev_globals_state_load(profiles_obj, "long-running",
						      &vdev->globals.long_running);
			if (arc != 0 && arc != -ENOENT) {
				rc = arc;
				goto _out;
			}

			for (si = 0; state_keys[si] != NULL; si++) {
				arc = vdev_globals_state_load_actions(
					&vdev->globals, profiles_obj, state_keys[si]);
				if (arc != 0) {
					rc = arc;
					goto _out;
				}
			}
		}
	}

	rc = 0;
_out:
	if (f != NULL)
		fclose(f);
	free(v);
	free(json);
	free(json_path);
	free(cfgdir);
	return rc == -ENOENT ? 0 : rc;
}
