/*
 * swmod_crun.c: SWMOD deamon
 *
 * Copyright (C) 2020-2023 IOPSYS Software Solutions AB. All rights reserved.
 *
 * Author: Suvendhu Hansa <suvendhu.hansa@iopsys.eu>
 *
 * See LICENSE file for license related information.
 */

#include <stdio.h>
#include <stdbool.h>
#include <unistd.h>
#include <sys/sysinfo.h>
#include <sys/utsname.h>
#include <sys/statvfs.h>

#include "tools.h"
#include "swmod_uci.h"
#include "swmod_crun.h"
#include "swmod_common.h"

static char* get_lxc_status(const char *c_name)
{
	if (c_name == NULL) {
		return "Error";
	}

	char cmd[MAX_BUFFER_LEN] = {0};
	char output[MAX_BUFFER_LEN] = {0};

	snprintf(cmd, sizeof(cmd), "lxc-info -s -n %s", c_name);

	if (run_cmd(cmd, output, sizeof(output)) != 0) {
		PRINT_ERR("Failed to read container status");
		return "Error";
	}

	if (strlen(output) == 0) {
		PRINT_ERR("Container status not found");
		return "Error";
	}

	if (strstr(output, "RUNNING") != NULL) {
		return "Up";
	}

	if (strstr(output, "STOPPED") != NULL) {
		return "Disabled";
	}

	return "Error";
}

static char* get_container_status(const char *c_name)
{
	if (c_name == NULL) {
		return "Error";
	}

	char cmd[MAX_BUFFER_LEN] = {0};
	char output[MAX_BUFFER_LEN] = {0};

	snprintf(cmd, sizeof(cmd), "crun state %s 2>&1 | grep status", c_name);

	if (run_cmd(cmd, output, sizeof(output)) != 0) {
		PRINT_ERR("Failed to read container status");
		return "Error";
	}

	if (strlen(output) == 0) {
		PRINT_ERR("Container status not found");
		return "Error";
	}

	if (strstr(output, "error opening") != NULL) {
		return "Error";
	}

	if (strstr(output, "\"status\": \"created\"") != NULL) {
		return "Disabled";
	}

	if (strstr(output, "\"status\": \"paused\"") != NULL) {
		return "Paused";
	}

	return "Up";
}

static int get_lxc_pid(const char *c_name)
{
	if (c_name == NULL) {
		return -1;
	}

	char cmd[MAX_BUFFER_LEN] = {0};
	char output[MAX_BUFFER_LEN] = {0};

	snprintf(cmd, sizeof(cmd), "lxc-info -p -n %s", c_name);

	if (run_cmd(cmd, output, sizeof(output)) != 0) {
		PRINT_ERR("Failed to read container pid");
		return -1;
	}

	if (strlen(output) == 0) {
		PRINT_DEBUG("Container pid not found");
		return -1;
	}

	if (strstr(output, "error opening") != NULL) {
		return -1;
	}

	int pid = -1;
	char *tmp = strstr(output, "PID:");
	if (tmp != NULL) {
		pid = atoi(output+4);
	}

	return pid;
}

static int get_container_pid(const char *c_name)
{
	if (c_name == NULL) {
		return -1;
	}

	char cmd[MAX_BUFFER_LEN] = {0};
	char output[MAX_BUFFER_LEN] = {0};

	snprintf(cmd, sizeof(cmd), "crun state %s 2>&1 | grep pid", c_name);

	if (run_cmd(cmd, output, sizeof(output)) != 0) {
		PRINT_ERR("Failed to read container pid");
		return -1;
	}

	if (strlen(output) == 0) {
		PRINT_DEBUG("Container pid not found");
		return -1;
	}

	if (strstr(output, "error opening") != NULL) {
		return -1;
	}

	int pid = -1;
	char *tmp = strstr(output, "\"pid\":");
	if (tmp != NULL) {
		sscanf(tmp, "\"pid\": %d", &pid);
	}

	return pid;
}

static int _lxc_run_cmd(const char *ct_name, char *sub_cmd, char *output, size_t len)
{
	if (ct_name == NULL || sub_cmd == NULL) {
		return -1;
	}

	char cmd[MAX_BUFFER_LEN] = {0};
	if ((strlen(ct_name) + strlen(sub_cmd) + 11) >= sizeof(cmd)) {
		PRINT_ERR("Command length too large");
		return -1;
	}

	snprintf(cmd, sizeof(cmd), "lxc-attach %s -- %s", ct_name, sub_cmd);
	return run_cmd(cmd, output, len);
}


static int crun_run_cmd(const char *ct_name, char *sub_cmd, char *output, size_t len)
{
	if (ct_name == NULL || sub_cmd == NULL) {
		return -1;
	}

	char cmd[MAX_BUFFER_LEN] = {0};
	if ((strlen(ct_name) + strlen(sub_cmd) + 11) >= sizeof(cmd)) {
		PRINT_ERR("Command length too large");
		return -1;
	}

	snprintf(cmd, sizeof(cmd), "crun exec %s %s", ct_name, sub_cmd);
	return run_cmd(cmd, output, len);
}

static int __run_cmd(const char *type, const char *ct_name, char *sub_cmd, char *output, size_t len)
{
	if (strcmp(type, "crun") == 0) {
		return crun_run_cmd(ct_name, sub_cmd, output, len);
	} else {
		return _lxc_run_cmd(ct_name, sub_cmd, output, len);
	}
	return -1;
}

static void get_memory_info(const char *type, const char *ct_name, int *avail_mem, int *alloc_mem)
{
	char cmd[1024] = {0};
	char output[1024] = {0};

	if (ct_name == NULL) {
		return;
	}

	snprintf(cmd, sizeof(cmd), "grep MemTotal /proc/meminfo");
	if (__run_cmd(type, ct_name, cmd, output, sizeof(output)) != 0) {
		return;
	}

	if (strstr(output, "MemTotal:") != NULL) {
		sscanf(output, "MemTotal:%d", alloc_mem);
	}

	memset(output, 0, sizeof(output));
	snprintf(cmd, sizeof(cmd), "grep MemFree /proc/meminfo");
	if (__run_cmd(type, ct_name, cmd, output, sizeof(output)) != 0) {
		return;
	}

	if (strstr(output, "MemFree:") != NULL) {
		sscanf(output, "MemFree:%d", avail_mem);
	}
}

static void get_disk_space_info(const char *type, const char *ct_name, int *avail_disk, int *alloc_disk)
{
	char cmd[1024] = {0};
	char output[MAX_BUFFER_LEN] = {0};

	if (ct_name == NULL) {
		return;
	}

	snprintf(cmd, sizeof(cmd), "df -P / | tail -n +2");
	if (__run_cmd(type, ct_name, cmd, output, sizeof(output)) != 0) {
		return;
	}

	if (strlen(output) == 0)
		return;

	char filesystem[255] = {0};
	int used_disk = 0;
	sscanf(output, "%254s %d %d %d", filesystem, alloc_disk, &used_disk, avail_disk);
}

static void get_host_info(const char *type, const char *ct_name, char *vendor, int vend_len, char *ver, int ver_len)
{
	char cmd[1024] = {0};
	char output[MAX_BUFFER_LEN] = {0};

	if (ct_name == NULL || vendor == NULL || ver == NULL) {
		return;
	}

	snprintf(cmd, sizeof(cmd), "uname -n");
	if (__run_cmd(type, ct_name, cmd, output, sizeof(output)) != 0) {
		return;
	}

	if (strlen(output) != 0) {
		strncpy(vendor, output, vend_len);
		remove_new_line(vendor);
	}

	memset(output, 0, sizeof(output));
	snprintf(cmd, sizeof(cmd), "uname -r");
	if (__run_cmd(type, ct_name, cmd, output, sizeof(output)) != 0) {
		return;
	}

	if (strlen(output) != 0) {
		strncpy(ver, output, ver_len);
		remove_new_line(ver);
	}
}

static bool validate_args(const char *url, char *output, int out_len)
{
	if (output == NULL || out_len <= 0) {
		return false;
	}

	if (url == NULL) {
		snprintf(output, out_len, "Package path not given");
		return false;
	}

	if (url[0] == '\0') {
		snprintf(output, out_len, "Package path not given");
		return false;
	}

	return true;
}

static int get_filename_from_url(const char *url, char *filename, int len)
{
	if (url == NULL || filename == NULL)
		return -1;

	memset(filename, 0, len);

	char *tmp = strrchr(url, '/');
	if (tmp == NULL) {
		tmp = (char*)url;
	} else {
		if (tmp == (url + strlen(url) - 1)) {
			return -1;
		} else {
			tmp = tmp + 1;
		}
	}

	snprintf(filename, len, "%s", tmp);
	char *tmp2 = strchr(filename, ':');
	if (tmp2)
		*tmp2 = '\0';

	tmp2 = strchr(filename, '.');
	if (tmp2)
		*tmp2 = '\0';

	tmp2 = strchr(filename, '@'); /* for docker image there could be @sha after name */
	if (tmp2)
		*tmp2 = '\0';

	return 0;
}

static int swmod_crun_install_tar_package(const PkgInfo *pkg, const char *name, char *output, int out_len)
{
	int ret = -1;
	char package_path[2049] = {0};
	bool remote_file = false;

	if (pkg == NULL || name == NULL || output == NULL) {
		return ret;
	}

	if (strlen(pkg->url) == 0) {
		snprintf(output, out_len, "Package URL not found");
		return ret;
	}

	if (strlen(pkg->uuid) == 0) {
		snprintf(output, out_len, "Package UUID not found");
		return ret;
	}

	if (pkg->section == NULL) {
		snprintf(output, out_len, "Internal Error");
		return ret;
	}

	swmod_uci_set_value_by_section(pkg->section, "du_status", "Installing_start");

	if (strstr(pkg->url, "file://") != NULL) {
		//local package
		snprintf(package_path, sizeof(package_path), "%s", pkg->url+6);
		if (strlen(package_path) == 0) {
			snprintf(output, out_len, "Invalid package name");
			return ret;
		}

		// get filesize
		unsigned long size = get_file_size_kb(package_path);
		if (size == 0) {
			snprintf(output, out_len, "Failed to read package info or package not exist");
			return ret;
		}

		/* we need atleast twice the pkg size space in bundle root to extract the tar */
		if (!memory_available(size * 2, swmod_config.oci_bundle_root)) {
			snprintf(output, out_len, "No enough space in system to extract the package");
			return ret;
		}
	} else {
		remote_file = true;
		unsigned long size = get_remote_file_size(pkg->url, pkg->uname, pkg->psw);
		if (size == 0) {
			snprintf(output, out_len, "Failed to read package info or package is empty");
			return ret;
		}

		if (!memory_available(size, DW_TMP_DIR_PATH)) {
			snprintf(output, out_len, "No enough space in system to download the package");
			return ret;
		}

		/* we need atleast twice the pkg size space in bundle root to extract the tar */
		if (!memory_available(size * 2, swmod_config.oci_bundle_root)) {
			snprintf(output, out_len, "No enough space in system to extract the package");
			return ret;
		}

		snprintf(package_path, sizeof(package_path), "%s/%s.tar", DW_TMP_DIR_PATH, name);
		if (0 != download_remote_package(pkg->url, pkg->uname, pkg->psw, package_path)) {
			snprintf(output, out_len, "Package download failed from %s", pkg->url);
			goto err;
		}
	}

	char bundle_path[MAX_LEN_1024] = {0};
	snprintf(bundle_path, sizeof(bundle_path), "%s/%s", swmod_config.oci_bundle_root, name);
	if (dir_exist(bundle_path)) {
		snprintf(output, out_len, "DU name %s already exist", name);
		goto err;
	}

	if (!create_dir(bundle_path)) {
		snprintf(output, out_len, "Failed to create dir %s", bundle_path);
		goto err;
	}

	char cmd[MAX_LEN_256] = {0};
	snprintf(cmd, sizeof(cmd), "tar -xf %s -C %s", package_path, bundle_path);
	if (0 != run_cmd_no_output(cmd)) {
		snprintf(output, out_len, "Failed to extract package at %s", bundle_path);
		snprintf(cmd, sizeof(cmd), "rm -rf %s", bundle_path);
		run_cmd_no_output(cmd);
		goto err;
	}

	ret = 0;
	snprintf(output, out_len, "Tar %s installed", name);

err:
	if (remote_file && (strlen(package_path) != 0) && file_exists(package_path))
		remove(package_path);

	return ret;
}

struct service_input_arg {
	const char *instance;
	char *error;
	int buf_size;
};

void read_fault_code(struct ubus_request *req, int type, struct blob_attr *msg)
{
	if (req == NULL || msg == NULL)
		return;

	struct service_input_arg *args = (struct service_input_arg *)req->priv;
	if (args == NULL)
		return;

	if (args->instance == NULL || args->error == NULL)
		return;

	snprintf(args->error, args->buf_size, "NoFault");

	enum {
		SERVICE_NAME,
		__SERVICE_MAX
	};

	enum {
		P_RUN,
		__P_MAX
	};

	const struct blobmsg_policy service_policy[__SERVICE_MAX] = {
		[SERVICE_NAME] = { .name = "crun", .type = BLOBMSG_TYPE_TABLE },
	};

	const struct blobmsg_policy p[__P_MAX] = {
		[P_RUN] = { .name = "running", .type = BLOBMSG_TYPE_BOOL },
	};

	struct blob_attr *tb[ARRAY_SIZE(service_policy)];

	if (blobmsg_parse(service_policy, ARRAY_SIZE(service_policy), tb, blob_data(msg), blob_len(msg)) != 0)
		return;

	if (!tb[SERVICE_NAME])
		return;

	struct blob_attr *data = tb[SERVICE_NAME];
	struct blob_attr *instances, *inst;
	size_t rem, rem2;

	blobmsg_for_each_attr(instances, data, rem) {
		if (strcmp(blobmsg_name(instances), "instances") != 0)
			continue;

		blobmsg_for_each_attr(inst, instances, rem2) {
			if (strcmp(blobmsg_name(inst), args->instance) != 0)
				continue;

			struct blob_attr *tb2[__P_MAX] = {NULL};
			if (blobmsg_parse(p, __P_MAX, tb2, blobmsg_data(inst), blobmsg_len(inst)) != 0)
				break;

			if (tb2[P_RUN] && blobmsg_get_bool(tb2[P_RUN]) == false)
				snprintf(args->error, args->buf_size, "FailureOnStart");

			break;
		}
		break;
	}
}

static void get_crun_fault_code(const char *name, char *fault, int buf_len)
{
	uint32_t id;
	struct service_input_arg input;

	if (fault == NULL)
		return;

	memset(fault, 0, buf_len);

	if (name == NULL)
		return;

	if (ubus_ctx == NULL)
		return;

	memset(&input, 0, sizeof(struct service_input_arg));
	input.instance = name;
	input.error = fault;
	input.buf_size = buf_len;

	struct blob_buf b;
	memset(&b, 0, sizeof(struct blob_buf));

	blob_buf_init(&b, 0);
	blobmsg_add_string(&b, "name", "crun");

	if (!ubus_lookup_id(ubus_ctx, "service", &id))
		ubus_invoke(ubus_ctx, id, "list", b.head, read_fault_code, &input, UBUS_TIMEOUT);

	blob_buf_free(&b);
}

int swmod_crun_perform_install(const PkgInfo *pkg, char *output, int out_len)
{
	int ret = -1;
	char filename[64] = {0};

	if (output == NULL)
		return ret;

	if (pkg == NULL) {
		snprintf(output, out_len, "Internal Error");
		return ret;
	}

	if (validate_args(pkg->url, output, out_len) == false)
		return ret;

	if (get_filename_from_url(pkg->url, filename, sizeof(filename)) != 0) {
		snprintf(output, out_len, "Invalid filename in %s", pkg->url);
		return ret;
	}

	char bundle_path[MAX_LEN_1024] = {0};
	snprintf(bundle_path, sizeof(bundle_path), "%s/%s", swmod_config.oci_bundle_root, filename);

	if (dir_exist(bundle_path)) {
		snprintf(output, out_len, "DU name %s already exist", filename);
		return ret;
	}

	snprintf(output, out_len, "Container %s to be installed", filename);
	ret = 0;

	char *tmp = strrchr(pkg->url, '.');
	if (tmp != NULL && strcmp(tmp, ".tar") == 0) {
		/* This is a tar package so need to extract */
		PRINT_DEBUG("Doing DU install with tar");
		ret = swmod_crun_install_tar_package(pkg, filename, output, out_len);
	}

	return ret;
}

int swmod_crun_perform_remove(const char *ct_name, char *output, int out_len)
{
	(void)ct_name;
	if (output == NULL || out_len == 0)
		return -1;

	// Bundle will be deleted externally
	snprintf(output, out_len, "Container %s uninstalled", ct_name);
	return 0;
}

void populate_crun_eu(ExecEnv_t *ee, const char *oci_uci_path)
{
	if (ee == NULL) {
		PRINT_ERR("No environment information given");
		return;
	}

	if (ee->name[0] == '\0') {
		PRINT_ERR("Environment name unknown");
		return;
	}

	if (oci_uci_path == NULL || strlen(oci_uci_path) == 0) {
		PRINT_ERR("OCI DU UCI file unknown");
		return;
	}

	if (0 != swmod_uci_init(oci_uci_path)) {
		return;
	}

	struct uci_section *s = NULL, *stmp = NULL;
	swmod_uci_foreach_section_safe(SWMOD_OCI_DU_UCI, "du_eu_assoc", stmp, s) {
		const char *type = swmod_uci_get_value_by_section(s, "type");
		const char *status = swmod_uci_get_value_by_section(s, "du_status");

		if ((strcmp(type, "crun") == 0 || strcmp(type, "lxc") == 0) && strcmp(status, "Installed") == 0) {
			ExecUnit new;
			memset(&new, 0, sizeof(ExecUnit));
			new.eu_exists = true;

			const char *name = swmod_uci_get_value_by_section(s, "eu_name");
			swmod_strncpy(new.name, name, sizeof(new.name));

			const char *alias = swmod_uci_get_value_by_section(s, "eu_alias");
			swmod_strncpy(new.eu_alias, alias, sizeof(new.eu_alias));

			const char *ee_name = swmod_uci_get_value_by_section(s, "ee_name");
			swmod_strncpy(new.environment, ee_name, sizeof(new.environment));

			const char *du_name = swmod_uci_get_value_by_section(s, "name");
			swmod_strncpy(new.du_name, du_name, sizeof(new.du_name));

			const char *req_state = swmod_uci_get_value_by_section(s, "requested_state");
			swmod_strncpy(new.req_state, req_state, sizeof(new.req_state));

			const char *auto_start = swmod_uci_get_value_by_section(s, "autostart");
			new.autostart = uci_str_to_bool(auto_start);

			int pid = -1;
			if (strcmp(type, "crun") == 0) {
				pid = get_container_pid(name);
			} else {
				pid = get_lxc_pid(name);
			}

			if (pid == -1) {
				snprintf(new.euid, sizeof(new.euid), "Unknown");
			} else {
				snprintf(new.euid, sizeof(new.euid), "%d", pid);
			}

			char *ct_status = NULL;
			if (strcmp(type, "crun") == 0) {
				ct_status = get_container_status(name);
			} else {
				ct_status = get_lxc_status(name);
			}

			if (strcmp(ct_status, "Up") == 0) {
				swmod_strncpy(new.state, "Active", sizeof(new.state));

				/* get memory space limits */
				int avail_memory = 0, alloc_memory = 0;
				get_memory_info(type, name, &avail_memory, &alloc_memory);
				new.memory_space = alloc_memory;

				/* get disk space info */
				int avail_disk = 0, alloc_disk = 0;
				get_disk_space_info(type, name, &avail_disk, &alloc_disk);
				new.disk_space = alloc_disk;

				/* get vendor and version info */
				get_host_info(type, name, new.vendor, sizeof(new.vendor), new.version, sizeof(new.version));
			} else {
				swmod_strncpy(new.state, "Idle", sizeof(new.state));
			}

			snprintf(new.description, sizeof(new.description), "This is a %s container", type);
			get_crun_fault_code(name, new.fault_code, sizeof(new.fault_code));
			swmod_add_eu_in_list(&ee->eu_list, &new);
		}
	}

	swmod_uci_fini(SWMOD_OCI_DU_UCI);
}

int swmod_set_crun_service_state(ExecEnv_t *ee, char *eu, bool state, const char *oci_uci_path)
{
	int ret = -1;

	if (ee == NULL) {
		PRINT_ERR("No environment information given");
		return ret;
	}

	if (ee->name[0] == '\0') {
		PRINT_ERR("Environment name unknown");
		return ret;
	}

	if (eu == NULL) {
		PRINT_ERR("No eu name information");
		return ret;
	}

	if (oci_uci_path == NULL || strlen(oci_uci_path) == 0) {
		PRINT_ERR("OCI DU uci path unknown");
		return ret;
	}

	if (0 != swmod_uci_init(oci_uci_path)) {
		return ret;
	}

	struct uci_section *s = NULL, *stmp = NULL;
	swmod_uci_foreach_section_safe(SWMOD_OCI_DU_UCI, "du_eu_assoc", stmp, s) {
		const char *name = swmod_uci_get_value_by_section(s, "eu_name");
		const char *ee_name = swmod_uci_get_value_by_section(s, "ee_name");

		if (strcmp(name, eu) == 0 && strcmp(ee->name, ee_name) == 0) {
			if (state) {
				swmod_uci_set_value_by_section(s, "requested_state", "Active");
				ret = 0;
			} else {
				swmod_uci_set_value_by_section(s, "requested_state", "Idle");
				ret = 0;
			}

			break;
		}
	}

	swmod_uci_fini(SWMOD_OCI_DU_UCI);
	if (ret == 0) {
		swmod_uci_commit("crun");
	}

	return ret;
}
