/*
 * Copyright (c) 2020 Red Hat.  All Rights Reserved.
 * 
 * This program 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.
 * 
 * This program 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.
 */
#include "pmlogconf.h"

static pmLongOptions longopts[] = {
    PMOPT_DEBUG,
    PMOPT_HOST,
    { "", 0, 'c', NULL, "add message and timestamp (not for interactive use)" },
    { "group", 1, 'g', "TAG", "report the logging state for a specific metrics group" },
    { "groups", 1, 'd', "DIR", "specify path to the pmlogconf groups directory" },
    { "option", 1, 'o', "TEXT", "text to add to the [options] section" },
    { "reprobe", 0, 'r', NULL, "every group reconsidered for inclusion in configfile" },
    { "verbose", 0, 'v', NULL, "increase diagnostic verbosity" },
    PMOPT_VERSION,
    PMOPT_HELP,
    PMAPI_OPTIONS_END
};

static int
override(int opt, pmOptions *opts)
{
    return (opt == 'g');
}

static pmOptions opts = {
    .short_options = "D:h:cd:g:ro:vV?",
    .long_options = longopts,
    .short_usage = "[options] ...",
    .override = override,
};

static char **options;
static char **metrics;
static unsigned int noptions;
static unsigned int nmetrics;

static int
pmrep_add(char ***array, unsigned int *n, char *string)
{
    unsigned int	count = *n;
    size_t		size = (count + 1) * sizeof(char *);
    char		**tmp;

    if ((tmp = realloc(*array, size)) == NULL)
	return -ENOMEM;
    tmp[count] = string;
    *n = count + 1;
    *array = tmp;
    return 0;
}

static void
ident_callback(const char *line, int length, void *arg)
{
    fprintf((FILE *)arg, "## %.*s\n", length, line);
}

static void
pmrep_group_ident(FILE *file, group_t *group)
{
    char	buffer[128];

    fmt(group->ident, buffer, sizeof(buffer), 65, 75, ident_callback, file);
}

static void
pmrep_group_enable(FILE *file, group_t *group)
{
    unsigned int	i;

    fprintf(file, "#+ %s:y:\n", group->tag);
    pmrep_group_ident(file, group);
    for (i = 0; i < group->nmetrics; i++)
	pmrep_add(&metrics, &nmetrics, group->metrics[i]);
}

static void
pmrep_group_disable(FILE *file, group_t *group)
{
    fprintf(file, "#+ %s:n:\n", group->tag);
    pmrep_group_ident(file, group);
}

static void
pmrep_group_exclude(FILE *file, group_t *group)
{
    fprintf(file, "#+ %s:x:\n", group->tag);
}

static void
pmrep_group_state(FILE *file, group_t *group)
{
    if (group->probe_state == STATE_INCLUDE)
	pmrep_group_enable(file, group);
    else if (group->probe_state == STATE_EXCLUDE)
	pmrep_group_exclude(file, group);
    else
	pmrep_group_disable(file, group);
}

static void
pmrep_create_group(FILE *file, group_t *group)
{
    evaluate_state(group);

    if (!prompt && !autocreate && group->probe_state != STATE_EXCLUDE &&
	(group->pmid != PM_ID_NULL || group->metric == NULL))
	fputc('.', stdout);

    pmrep_group_state(file, group);
    fputs("#----\n", file);
}

static void
pmrep_header(FILE *f)
{
    unsigned int	i;

    if (header) {
	fputs(header, f);
	return;
    }

    fprintf(f,
"#pmrepconf 1.0\n"
"#\n"
"# pmrep.conf(5) config file created and updated by pmrepconf\n");

    if (autocreate) {
	time_t	timenow = time(NULL);
	fprintf(f,
"# Auto-generated by %s on: %s\n", pmGetProgname(), ctime(&timenow));
    }

    fprintf(f,
"#\n"
"# DO NOT UPDATE THE INITIAL SECTION OF THIS FILE.\n"
"# Any changes may be lost the next time pmrepconf is used\n"
"# on this file.\n"
"#\n"
"#+ groupdir %s\n"
"#\n", groupdir);

    fputs("\n[options]\n", f);
    fputs("version=1\n", f);
    fputs("ignore_unknown=yes\n", f);
    for (i = 0; i < noptions; i++)
	fprintf(f, "%s\n", options[i]);
    fputs("\n", f);
}

static int
name_compare(const void *p, const void *q)
{
    const char		**pp = (const char **)p;
    const char		**qq = (const char **)q;

    return strcmp(*pp, *qq);
}

static void
name_rewrite(char *in, char *out, size_t outsz)
{
    char *ip, *op;

    /* Metrics ending in keywords (like 'filesys.type') are problematic */
    for (ip = in, op = out; *ip != '\0' && op - out < outsz-2; ip++, op++)
	*op = (*ip == '.') ? '_' : *ip;
    out[ip - in] = '\0';
}

static void
pmrep_metrics(FILE *f)
{
    unsigned int	i;
    char		*s;
    char		buffer[BUFSIZ];

    /* cleanup metric lines, removing comments, whitespace */
    for (i = 0; i < nmetrics; i++) {
	for (s = metrics[i]; *s; s++) {
	    if (isspace(*s) || *s == '#') {
		*s = '\0';
		break;
	    }
	}
    }

    /* sort the names so we can remove duplicates below */
    qsort(metrics, nmetrics, sizeof(char *), name_compare);

    fputs("\n[metrics]\n", f);
    for (i = 0; i < nmetrics; i++) {
	if (*metrics[i] == '\0')
	    continue;
	if (i > 0 && strcmp(metrics[i], metrics[i-1]) == 0)
	    continue;	/* skip duplicates */
	name_rewrite(metrics[i], buffer, sizeof(buffer));
	fprintf(f, "%s = %s\n", buffer, metrics[i]);
    }
    fputs("\n", f);
}

static void
pmrep_trailer(FILE *f)
{
    if (trailer) {
	fputs(trailer, f);
	return;
    }

    fprintf(f,
"# DO NOT UPDATE THE FILE ABOVE THIS LINE\n"
"# Otherwise any changes may be lost the next time pmrepconf is\n"
"# used on this file.\n"
"#\n"
"# It is safe to make additions from here on ...\n"
"#\n");
}

static void
pmrep_report_group(group_t *group)
{
    unsigned int	state;

    if (!group->valid)
	return;

    if (finaltag == NULL && setupfile == NULL)
	finaltag = "all";	/* verbose mode default */

    if (finaltag) {
	if (strcmp(finaltag, "all") != 0 && strcmp(finaltag, group->tag) != 0)
	    return;
	if ((state = group->saved_state) == 0)
	    state = group->probe_state;
    } else if (setupfile) {
	state = group->probe_state;
    } else
	return;

    if (verbose) {
	printf("%s/%s:\n", groupdir, group->tag);
	if (group->force)
	    printf("%s -> ", group->force);
	else
	    printf("%s -> probe=%s ", group->probe,
		    group->success? "success" : "failure");
	if (state == STATE_EXCLUDE)
	    printf("action=exclude\n");
	else if (state == STATE_INCLUDE)
	    printf("action=include\n");
	else
	    printf("action=available\n");
    }

    if (state == STATE_EXCLUDE)
	printf("#+ %s/%s:x:\n", groupdir, group->tag);
    else if (state == STATE_INCLUDE)
	printf("#+ %s/%s:y:\n", groupdir, group->tag);
    else
	printf("#+ %s/%s:n:\n", groupdir, group->tag);
}

static void
pmrep_create(FILE *file)
{
    FILE		*tempfile;
    unsigned int	i;

    printf("Creating config file \"%s\" using default settings ...\n", config);
    parse_groups(groupdir, NULL);
    fetch_groups();
    setup_groups();

    if (finaltag) {
	for (i = 0; i < ngroups; i++)
	    pmrep_report_group(&groups[i]);
	return;
    }

    create_tempfile(file, &tempfile, NULL);
    pmrep_header(tempfile);
    for (i = 0; i < ngroups; i++) {
	pmrep_create_group(tempfile, &groups[i]);
	if (verbose)
	    pmrep_report_group(&groups[i]);
    }
    pmrep_metrics(tempfile);
    pmrep_trailer(tempfile);

    if (rename(tmpconfig, config) < 0) {
	fprintf(stderr, "%s: cannot rename file %s to %s: %s\n",
		pmGetProgname(), tmpconfig, config, osstrerror());
	exit(EXIT_FAILURE);
    }
    fclose(tempfile);
    fputc('\n', stdout);
}

static void
pmrep_update_group(FILE *file, group_t *group)
{
    if (group->valid) {
	evaluate_state(group);
	pmrep_group_state(file, group);
	fputs("#----\n", file);
    }
}

static void
update_pmrep_tempfile(FILE *tempfile)
{
    unsigned int	i;
    char		*pattern = NULL;
    char		answer[16] = {0};

    /* Interactive loop (unless 'prompt' variable is cleared) */
    do {
	if ((pattern = update_groups(tempfile, pattern)) == NULL)
	    break;
	printf(" not found.\n");
	for (;;) {
	    printf("Continue searching from start of the file? [y] ");
	    if (fgets(answer, sizeof(answer), stdin) == NULL)
		answer[0] = 'y';
	    if (answer[0] == 'y' || answer[0] == 'n')
		break;
	    printf("Error: you must answer \"y\" or \"n\" ... try again\n");
	}
	if (answer[0] == 'n') {
	    pattern = NULL;
	    prompt = 1;
	} else {
	    printf("Searching for \"%s\"\n", pattern);
	}
    } while (1);

    /*
     * If we are reprobing, or any group exists in pmlogconf files but not
     * the pmrep file, make sure we rewrite the pmrep configuration.
     */
    if (reprobe) {
	rewrite = 1;
    } else if (rewrite == 0) {
	for (i = 0; i < ngroups; i++)
	    if (groups[i].pmlogconf && !groups[i].pmrep) {
		rewrite = 1;
		break;
	    }
    }

    /*
     * Finally write out the pmrep configuration if anything has been
     * modified - either by the user or by new pmlogconf groups arriving.
     */
    if (rewrite) {
	if (ftruncate(fileno(tempfile), 0L) < 0)
	    fprintf(stderr, "%s: cannot truncate temporary file: %s\n",
			pmGetProgname(), osstrerror());
	if (fseek(tempfile, 0L, SEEK_SET) < 0)
	    fprintf(stderr, "%s: cannot fseek to temporary file start: %s\n",
			pmGetProgname(), osstrerror());
	prompt = 0;
	pmrep_header(tempfile);
	for (i = 0; i < ngroups; i++)
	    pmrep_update_group(tempfile, &groups[i]);
	pmrep_metrics(tempfile);
	pmrep_trailer(tempfile);
	fflush(tempfile);
    }
}

static group_t *
group_create_pmrep(const char *state, unsigned int line)
{
    size_t		length;
    group_t		*group;
    const char		*begin, *end;
    enum { TAG, STATE } parse;

    if ((group = calloc(1, sizeof(group_t))) == NULL) {
	fprintf(stderr, "%s: cannot create group for %s: %s\n",
			pmGetProgname(), state, osstrerror());
	exit(EXIT_FAILURE);
    }
    group->pmrep = 1;

    /*
     * pmrep group control lines have this format
     * #+ tag:on-off
     * where
     *       tag     is arbitrary (no embedded :'s) and unique
     *       on-off  y or n to enable or disable this group, else
     *               x for groups excluded by probing when the group
     *               was added to the configuration file
     */

    parse = TAG;
    begin = end = state;
    while (*end != '\n' && *end != '\0') {
	if (*end != ':') {
	    end++;
	    continue;
	}
	length = end - begin;
	switch (parse) {
	case TAG:
	    if (length > 0) {
		if ((group->tag = strndup(begin, length)) == NULL) {
		    fprintf(stderr, "%s: out-of-memory parsing %s at line %u\n",
			    pmGetProgname(), config, line);
		    exit(EXIT_FAILURE);
		}
	    } else {
		fprintf(stderr, "%s: missing tag for group at line %u of %s\n",
			pmGetProgname(), line, config);
		return group_free(group);
	    }
	    break;

	case STATE:
	    if (strncmp(begin, "y", length) == 0)
		group->saved_state = STATE_INCLUDE;
	    else if (strncmp(begin, "n", length) == 0)
		group->saved_state = STATE_AVAILABLE;
	    else if (strncmp(begin, "x", length) == 0)
		group->saved_state = STATE_EXCLUDE;
	    else {
		fprintf(stderr, "%s: missing state for %s at line %u of %s\n",
			pmGetProgname(), group->tag, line, config);
		return group_free(group);
	    }
	    break;

	default:
	    break;
	}
	begin = ++end;
	parse++;
    }
    if (group->tag != NULL)
	return group;
    return group_free(group);
}

static void
copy_and_parse_tempfile(FILE *file, FILE *tempfile)
{
    char		bytes[BUFSIZ];
    group_t		*group = NULL;
    unsigned int	tail = 0, head = 0, line = 0, count = 0;

    /*
     * Copy the contents of config into tempfile, parsing and extracting
     * existing group state as we go, and stashing the immutable trailer
     * as well.
     */
    fseek(file, 0L, SEEK_SET);
    while (fgets(bytes, sizeof(bytes), file) != NULL) {
	fputs(bytes, tempfile);	/* copy into temporary configuration file */
	line++;

	if (strncmp(bytes, "#pmrepconf 1", 12) == 0)
	    head = 1;
	if (strncmp(bytes, "# DO NOT UPDATE THE FILE ABOVE THIS LINE", 40) == 0)
	    tail = 1;

	if (tail) {
	    trailer = append(trailer, bytes, '\n');
	} else if (strncmp("#+ groupdir ", bytes, 12) == 0) {
	    group_dircheck(bytes + 12);
	} else if (strncmp("#+ ", bytes, 3) == 0) {
	    if (group)
	    	group_free(group);
	    group = group_create_pmrep(bytes + 3, line);
	    head = 0;
	} else if (group) {
	    if (strncmp("#----", bytes, 5) == 0)
		group = group_finish(group, &count);
	    else if (strncmp("## ", bytes, 3) == 0)
		group_ident(group, bytes + 3);
	    head = 0;
	}

	if (head)
	    header = append(header, bytes, '\n');
    }
    if (group)	/* missing end marker? - we'll optimistically fix this up */
	group_finish(group, &count);
    fflush(tempfile);
}

static void
pmrep_update(FILE *file, struct stat *stat)
{
    FILE		*tempfile;
    unsigned int	i;

    if (host && !reprobe)
	fprintf(stderr,
		"%s: Warning: existing config file, -h %s will be ignored\n",
		pmGetProgname(), host);

    parse_groups(groupdir, NULL);
    fetch_groups();
    setup_groups();

    create_tempfile(file, &tempfile, stat);
    copy_and_parse_tempfile(file, tempfile);

    if (finaltag) {
	for (i = 0; i < ngroups; i++)
	    pmrep_report_group(&groups[i]);
	unlink(tmpconfig);
	fclose(tempfile);
	return;
    }

    update_pmrep_tempfile(tempfile);

    if (verbose) {
	for (i = 0; i < ngroups; i++)
	    pmrep_report_group(&groups[i]);
    }
    diff_tempfile(tempfile);
}

int
pmrepconf(int argc, char **argv)
{
    struct stat	sbuf;
    FILE	*file;
    int		c;

    while ((c = pmGetOptions(argc, argv, &opts)) != EOF) {
	switch (c) {

	case 'c':	/* auto-generated */
	    autocreate = 1;
	    prompt = 0;
	    break;

	case 'd':
	    pmsprintf(groupdir, MAXPATHLEN, "%s", opts.optarg);
	    break;

	case 'g':	/* group (report final configured states) */
	    finaltag = opts.optarg;
	    prompt = 0;
	    break;

	case 'o':	/* [option] section entry */
	    pmrep_add(&options, &noptions, opts.optarg);
	    break;

	case 'r':	/* reprobe */
	    reprobe = 1;
	    break;

	case 'v':	/* verbose */
	    verbose = 1;
	    break;	
	}
    }

    if (autocreate && finaltag) {
	pmprintf("Option -c cannot be used with -g,--group\n");
	opts.errors++;
    }

    if (opts.errors || opts.optind != argc - 1) {
	pmUsageMessage(&opts);
	exit(EXIT_FAILURE);
    }

    if (opts.flags & PM_OPTFLAG_EXIT) {
	pmflush();
	pmUsageMessage(&opts);
	exit(0);
    }

    pmapi_setup(&opts);

    group_setup();

    /* given pmrep configuration file to be created or modified */
    config = argv[opts.optind];
    setoserror(0);
    if ((file = fopen(config, finaltag ? "r" : "r+")) == NULL) {
	if (oserror() == ENOENT)
	    file = fopen(config, "w+");
	if (file == NULL) {
	    fprintf(stderr, "%s: Cannot open pmrep configuration \"%s\": %s\n",
			pmGetProgname(), config,  osstrerror());
	    exit(EXIT_FAILURE);
	}
    }
    if (fstat(fileno(file), &sbuf) < 0)
	existing = 0;
    else if (sbuf.st_size > 0)
	existing = 1;
    else
	existing = 0;
    if (existing == 0)
	prompt = 0;
    deltas = 0;

    if (!existing)
	pmrep_create(file);
    else
	pmrep_update(file, &sbuf);
    fclose(file);
    return 0;
}
