/*****************************************************************************\
 *  scrontab.c
 *****************************************************************************
 *  Copyright (C) SchedMD LLC.
 *
 *  This file is part of Slurm, a resource management program.
 *  For details, see <https://slurm.schedmd.com/>.
 *  Please also read the included file: DISCLAIMER.
 *
 *  Slurm is free software; you can redistribute it and/or modify it under
 *  the terms of the GNU General Public License as published by the Free
 *  Software Foundation; either version 2 of the License, or (at your option)
 *  any later version.
 *
 *  In addition, as a special exception, the copyright holders give permission
 *  to link the code of portions of this program with the OpenSSL library under
 *  certain conditions as described in each individual source file, and
 *  distribute linked combinations including the two. You must obey the GNU
 *  General Public License in all respects for all of the code used other than
 *  OpenSSL. If you modify file(s) with this exception, you may extend this
 *  exception to your version of the file(s), but you are not obligated to do
 *  so. If you do not wish to do so, delete this exception statement from your
 *  version.  If you delete this exception statement from all source files in
 *  the program, then also delete it here.
 *
 *  Slurm is distributed in the hope that it will be useful, 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.
 *
 *  You should have received a copy of the GNU General Public License along
 *  with Slurm; if not, write to the Free Software Foundation, Inc.,
 *  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301  USA.
\*****************************************************************************/

#include <ctype.h>
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>


#include "src/common/bitstring.h"
#include "src/interfaces/cli_filter.h"
#include "src/interfaces/hash.h"
#include "src/common/cron.h"
#include "src/common/env.h"
#include "src/common/fetch_config.h"
#include "src/common/log.h"
#include "src/common/ref.h"
#include "src/common/read_config.h"
#include "src/common/slurm_opt.h"
#include "src/common/spank.h"
#include "src/common/uid.h"
#include "src/common/xmalloc.h"
#include "src/common/xstring.h"

#include "scrontab.h"

#define OPT_LONG_AUTOCOMP 0x100

static void _usage(void);

decl_static_data(default_crontab_txt);
decl_static_data(usage_txt);

static uid_t uid;
static gid_t gid;
static bool edit_only = false;
static bool first_form = false;
static bool list_only = false;
static bool remove_only = false;

/* Name of input file. May be "-". */
static const char *infile = NULL;

scron_opt_t scopt;
slurm_opt_t opt = {
	.scron_opt = &scopt,
	.help_func = _usage,
	.usage_func = _usage
};

static void _usage(void)
{
	char *txt;
	static_ref_to_cstring(txt, usage_txt);
	fprintf(stderr, "%s", txt);
	xfree(txt);
}

static void _parse_args(int argc, char **argv)
{
	log_options_t logopt = LOG_OPTS_STDERR_ONLY;
	int c = 0, option_index = 0;

	static struct option long_options[] = {
		{"autocomplete", required_argument, 0, OPT_LONG_AUTOCOMP},
		{NULL, no_argument, 0, 'e'},
		{NULL, no_argument, 0, 'l'},
		{NULL, no_argument, 0, 'r'},
		{NULL, required_argument, 0, 'u'},
		{NULL, no_argument, 0, 'v'},
		{NULL, 0, 0, 0}};

	log_init(xbasename(argv[0]), logopt, 0, NULL);
	uid = getuid();
	gid = getgid();

	opterr = 0;
	while ((c = getopt_long(argc, argv, "elru:v",
				long_options, &option_index)) != -1) {
		switch (c) {
		case (int)'e':
			if (!isatty(STDIN_FILENO))
				fatal("Standard input is not a TTY");
			edit_only = true;
			break;
		case (int)'l':
			list_only = true;
			break;
		case (int)'r':
			remove_only = true;
			break;
		case (int)'u':
			if (uid_from_string(optarg, &uid))
				fatal("Could not find user %s", optarg);
			gid = gid_from_uid(uid);
			break;
		case (int)'v':
			logopt.stderr_level++;
			log_alter(logopt, 0, NULL);
			break;
		case OPT_LONG_AUTOCOMP:
			suggest_completion(long_options, optarg);
			exit(0);
			break;
		default:
			_usage();
			exit(1);
		}
	}

	if (edit_only || list_only || remove_only) {
		if (optind < argc) {
			_usage();
			exit(1);
		}
		return;
	}

	if ((argc - optind) > 1) {
		_usage();
		exit(1);
	}

	if (optind < argc) {
		first_form = true;
		infile = argv[optind];
	} else if (isatty(STDIN_FILENO)) {
		edit_only = true;
	} else {
		first_form = true;
		infile = "-";
	}
}

static void _update_crontab_with_disabled_lines(char **crontab,
						char *disabled_lines,
						char *prepend)
{
	char **lines;
	int line_count = 0;
	bitstr_t *disabled;
	char *new_crontab = NULL;

	if (!*crontab || !disabled_lines || disabled_lines[0] == '\0')
		return;

	lines = convert_file_to_line_array(*crontab, &line_count);
	disabled = bit_alloc(line_count);

	bit_unfmt(disabled, disabled_lines);

	for (int i = 1; lines[i]; i++) {
		if (bit_test(disabled, i))
			xstrfmtcat(new_crontab, "%s%s\n", prepend, lines[i]);
		else
			xstrfmtcat(new_crontab, "%s\n", lines[i]);
	}
	xfree(*crontab);
	*crontab = new_crontab;
	FREE_NULL_BITMAP(disabled);
	xfree(lines);
}

static void _reset_options(void)
{
	slurm_reset_all_options(&opt, true);
	/* cli_filter plugins can change the defaults */
	if (cli_filter_g_setup_defaults(&opt, false)) {
		error("cli_filter plugin terminated with error");
		exit(1);
	}

	opt.job_flags |= CRON_JOB;
}

static char *_job_script_header(void)
{
	return xstrdup("#!/bin/sh\n"
		       "# This job was submitted through scrontab\n");
}

static char *_read_fd(int fd)
{
	char *buf, *ptr;
	int buf_size = 4096;
	size_t buf_left;
	ssize_t script_size = 0, tmp_size;
	buf = ptr = xmalloc(buf_size);
	buf_left = buf_size;

	while ((tmp_size = read(fd, ptr, buf_left)) > 0) {
		buf_left -= tmp_size;
		script_size += tmp_size;
		if (buf_left == 0) {
			buf_size += BUFSIZ;
			xrealloc(buf, buf_size);
		}
		ptr = buf + script_size;
		buf_left = buf_size - script_size;
	}

	return buf;
}

static char *_load_script_from_fd(int fd)
{
	if (lseek(fd, 0, SEEK_SET) < 0)
		fatal("%s: lseek(0): %m", __func__);

	return _read_fd(fd);
}

static char *_tmp_path(void)
{
	char *tmpdir = getenv("TMPDIR");
	return tmpdir ? tmpdir : "/tmp";
}

/*
 * Replace crontab with an edited version after running an editor.
 */
static void _edit_crontab(char **crontab)
{
	int fd, wstatus, err;
	pid_t pid;
	char *editor, *filename = NULL;

	if (!*crontab)
		static_ref_to_cstring(*crontab, default_crontab_txt);

	xstrfmtcat(filename, "%s/scrontab-XXXXXX", _tmp_path());

	/* protect against weak file permissions in old glibc */
	umask(0077);
	fd = mkstemp(filename);
	if (fd < 0)
		fatal("error creating temp crontab file '%s': %m", filename);
	safe_write(fd, *crontab, strlen(*crontab));
	close(fd);

	xfree(*crontab);

	if (!(editor = getenv("VISUAL")) || (editor[0] == '\0'))
		if (!(editor = getenv("EDITOR")) || (editor[0] == '\0'))
			editor = "vi";

	if ((pid = fork()) == -1) {
		unlink(filename);
		xfree(filename);
		fatal("cannot fork");
	}

	if (!pid) {
		/* child */
		char *argv[3];
		argv[0] = editor;
		argv[1] = filename;
		argv[2] = NULL;

		execvp(editor, argv);
		exit(127);
	}

	waitpid(pid, &wstatus, 0);

	if (wstatus) {
		unlink(filename);
		xfree(filename);
		fatal("editor returned non-zero exit code");
	}

	fd = open(filename, O_RDONLY);
	if (fd < 0) {
		err = errno;
		unlink(filename);
		fatal("error reopening temp crontab file '%s': %s",
		      filename, strerror(err));
	}

	*crontab = _load_script_from_fd(fd);
	close(fd);
	unlink(filename);
	xfree(filename);
	return;

rwfail:
	err = errno;
	close(fd);
	unlink(filename);
	fatal("error writing to temp crontab file '%s': %s",
	      filename, strerror(err));
}

static job_desc_msg_t *_entry_to_job(cron_entry_t *entry, char *script)
{
	job_desc_msg_t *job = xmalloc(sizeof(*job));

	slurm_init_job_desc_msg(job);
	fill_job_desc_from_opts(job);

	job->crontab_entry = entry;

	/* finish building the batch script */
	xstrfmtcat(script, "# crontab time request was: '%s'\n%s\n",
		   entry->cronspec, entry->command);
	job->script = script;

	job->environment = env_array_create();
	set_prio_process_env();
	env_array_overwrite(&job->environment, "SLURM_PRIO_PROCESS",
			    getenv("SLURM_PRIO_PROCESS"));
	env_array_overwrite(&job->environment, "SLURM_GET_USER_ENV", "1");
	job->env_size = envcount(job->environment);

	if (!job->name) {
		char *pos;
		job->name = xstrdup(entry->command);
		if ((pos = xstrstr(job->name, " ")))
			*pos = '\0';
	}

	if (!job->work_dir) {
		if (!(job->work_dir = uid_to_dir(uid)))
			fatal("uid_to_dir(%u) failed", uid);
	}

	return job;
}

static int _list_find_key(void *x, void *key)
{
	config_key_pair_t *key_pair = x;
	char *str = key;

	if (!xstrcmp(key_pair->name, str))
		return 1;

	return 0;
}

static int _foreach_env_var_expand(void *x, void *arg)
{
	char *key = NULL;
	config_key_pair_t *key_pair = x;
	cron_entry_t *entry = arg;

	xstrfmtcat(key, "$%s", key_pair->name);
	xstrsubstitute(entry->command, key, key_pair->value);
	xfree(key);

	return SLURM_SUCCESS;
}

static void _expand_variables(cron_entry_t *entry, list_t *env_vars)
{
	if (!env_vars || !list_count(env_vars))
		return;

	list_for_each(env_vars, _foreach_env_var_expand, entry);
}

static void _edit_and_update_crontab(char *crontab)
{
	char **lines;
	char *badline = NULL;
	int lineno, line_start;
	char *line;
	list_t *jobs, *env_vars;
	int line_count;
	bool setup_next_entry = true;
	char *script;
	crontab_update_response_msg_t *response;

edit:
	if (edit_only && crontab) {
		slurm_hash_t before = { .type = HASH_PLUGIN_K12 };
		slurm_hash_t after = { .type = HASH_PLUGIN_K12 };
		int before_len, after_len;

		before_len = hash_g_compute(crontab, strlen(crontab), NULL, 0,
					   &before);
		_edit_crontab(&crontab);
		after_len = hash_g_compute(crontab, strlen(crontab), NULL, 0,
					   &after);
		if ((before_len == after_len) &&
		    (!memcmp(before.hash, after.hash, before_len))) {
			info("No modification made");
			xfree(crontab);
			return;
		}
	} else if (edit_only) {
		_edit_crontab(&crontab);
	}

	jobs = list_create((ListDelF) slurm_free_job_desc_msg);
	env_vars = list_create(destroy_config_key_pair);
	lines = convert_file_to_line_array(xstrdup(crontab), &line_count);

	lineno = 0;
	line_start = -1;
	setup_next_entry = true;
	while ((line = lines[lineno])) {
		char *pos = line;
		cron_entry_t *entry;

		if (setup_next_entry) {
			_reset_options();
			script = _job_script_header();
			setup_next_entry = false;
		}

		/* advance to the first non-whitespace character */
		while (*pos == ' ' || *pos == '\t')
			pos++;

		if (*pos == '\0' || *pos == '\n') {
			/* line was only whitespace */
			lineno++;
			continue;
		} else if (!xstrncmp(pos, "#SCRON", 6)) {
			xstrfmtcat(script, "%s\n", line);
			if (line_start == -1)
				line_start = lineno;
			if (parse_scron_line(line + 6, lineno)) {
				badline = xstrdup_printf("%d", lineno);
				break;
			}
			lineno++;
			continue;
		} else if (*pos == '#') {
			/* boring comment line */
			lineno++;
			continue;
		} else {
			/* check env setting. */
			char *name = NULL, *value = NULL;

			if (load_env(pos, &name, &value)) {
				config_key_pair_t *key_pair =
					xmalloc(sizeof(*key_pair));

				xassert(name);
				xassert(value);

				key_pair->name = name;
				key_pair->value = value;
				list_delete_all(env_vars, _list_find_key, name);
				list_append(env_vars, key_pair);

				lineno++;
				continue;
			}
		}

		if (!(entry = cronspec_to_bitstring(pos))) {
			badline = xstrdup_printf("%d", lineno);
			break;
		}

		_expand_variables(entry, env_vars);

		if (cli_filter_g_pre_submit(&opt, 0)) {
			free_cron_entry(entry);
			badline = xstrdup_printf("%d-%d", line_start, lineno);
			printf("cli_filter plugin terminated with error\n");
			break;
		}

		/*
		 * track lines associated with this job submission, starting
		 * starting at the first SCRON directive and completing here
		 */
		if (line_start != -1)
			entry->line_start = line_start;
		else
			entry->line_start = lineno;
		line_start = -1;

		entry->line_end = lineno;

		list_append(jobs, _entry_to_job(entry, script));
		script = NULL;
		setup_next_entry = true;
		lineno++;
	}
	xfree(*(lines + 1));
	xfree(lines);
	xfree(script);
	FREE_NULL_LIST(env_vars);

	if (badline) {
		if (first_form) {
			printf("There are errors in your crontab.\n");
			xfree(badline);
			xfree(crontab);
			FREE_NULL_LIST(jobs);
			exit(1);
		}

		char c = '\0';

		FREE_NULL_LIST(jobs);

		while (tolower(c) != 'y' && tolower(c) != 'n') {
			printf("There are errors in your crontab.\n"
			       "The failed line(s) is commented out with #BAD:\n"
			       "Do you want to retry the edit? (y/n) ");
			c = (char) getchar();
		}

		if (c == 'n')
			exit(0);

		_update_crontab_with_disabled_lines(&crontab, badline,
						    "#BAD: ");
		xfree(badline);

		goto edit;
	}

	response = slurm_update_crontab(uid, gid, crontab, jobs);

	if (response->return_code) {
		if (first_form) {
			printf("There was an issue with the job submission on lines %s\n"
				"The error code return was: %s\n"
				"The error message was: %s\n",
				response->failed_lines,
				slurm_strerror(response->return_code),
				response->err_msg);
			slurm_free_crontab_update_response_msg(response);
			xfree(crontab);
			FREE_NULL_LIST(jobs);
			exit(1);
		}

		char c = '\0';
		FREE_NULL_LIST(jobs);
		while (tolower(c) != 'y' && tolower(c) != 'n') {
			printf("There was an issue with the job submission on lines %s\n"
			       "The error code return was: %s\n"
			       "The error message was: %s\n"
			       "The failed lines are commented out with #BAD:\n"
			       "Do you want to retry the edit? (y/n) ",
			       response->failed_lines,
			       slurm_strerror(response->return_code),
			       response->err_msg);
			c = (char) getchar();
		}

		if (c == 'n')
			exit(0);

		_update_crontab_with_disabled_lines(&crontab,
						    response->failed_lines,
						    "#BAD: ");
		slurm_free_crontab_update_response_msg(response);
		goto edit;
	}

	if (response->job_submit_user_msg)
		print_multi_line_string(response->job_submit_user_msg, -1,
					LOG_LEVEL_INFO);

	for (int i = 0; i < response->jobids_count; i++)
		cli_filter_g_post_submit(0, response->jobids[i], NO_VAL);

	slurm_free_crontab_update_response_msg(response);
	xfree(crontab);
	FREE_NULL_LIST(jobs);
}

static void _handle_first_form(char **new_crontab)
{
	int input_desc;

	if (!infile)
		fatal("invalid input file");

	if (!xstrcmp(infile, "-")) {
		input_desc = STDIN_FILENO;
		*new_crontab = _read_fd(input_desc);
	} else {
		if ((input_desc = open(infile, O_RDONLY)) < 0)
			fatal("failed to open %s", infile);

		*new_crontab = _read_fd(input_desc);
		if (close(input_desc))
			fatal("failed to close fd %d", input_desc);
	}
}


extern int main(int argc, char **argv)
{
	int rc;
	char *crontab = NULL, *disabled_lines = NULL;

	slurm_init(NULL);
	_parse_args(argc, argv);

	if (cli_filter_init() != SLURM_SUCCESS)
		fatal("failed to initialize cli_filter plugin");

	if (!xstrcasestr(slurm_conf.scron_params, "enable"))
		fatal("scrontab is disabled on this cluster");

	if (first_form)
		_handle_first_form(&crontab);

	if (remove_only) {
		if ((rc = slurm_remove_crontab(uid, gid)))
			fatal("slurm_remove_crontab failed: %s",
			      slurm_strerror(rc));
		exit(0);
	}

	if (edit_only || list_only) {
		if ((rc = slurm_request_crontab(uid, &crontab,
						&disabled_lines)) &&
		    (rc != ESLURM_JOB_SCRIPT_MISSING)) {
			fatal("slurm_request_crontab failed: %s",
			      slurm_strerror(rc));
		}

		_update_crontab_with_disabled_lines(&crontab, disabled_lines,
						    "#DISABLED: ");
		xfree(disabled_lines);
	}

	if (list_only) {
		if (!crontab) {
			printf("no crontab for %s\n", uid_to_string(uid));
			exit(1);
		}

		printf("%s", crontab);
		xfree(crontab);
		exit(0);
	}

	/* needed otherwise slurm_option_table_create() always returns NULL */
	if (spank_init_allocator())
		fatal("failed to initialize plugin stack");

	_edit_and_update_crontab(crontab);

	cli_filter_fini();
	spank_fini(NULL);

	return 0;
}
