/* This file is part of Mailfromd.
   Copyright (C) 2005-2025 Sergey Poznyakoff

   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 3, 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.

   You should have received a copy of the GNU General Public License
   along with this program.  If not, see <http://www.gnu.org/licenses/>. */

#ifdef HAVE_CONFIG_H
# include <config.h>
#endif

#include <unistd.h>
#include <stdlib.h>
#include <sysexits.h>

#include <mailutils/mailutils.h>
#include <mailutils/cli.h>
#include <mailutils/daemon.h>

#include "libmf.h"
#include "mfdb.h"
#include "filenames.h"

char *state_dir = DEFAULT_STATE_DIR;
char *file_option;               /* File name for DB management commands */
struct db_format *format_option;
time_t expire_interval = 0;      /* When set, overrides the expire interval
				    for the selected database */
int all_option;                  /* Process all databases */
char *db_type_str = DEFAULT_DB_TYPE;

void (*run)(int argc, char **argv) = NULL;

void
assert_db_format()
{
	if (!format_option) {
		mu_error(_("operation is not applicable"));
		exit(EX_USAGE);
	}
}

char *
get_db_name()
{
	assert_db_format();
	if (file_option)
		return file_option;
	if (format_option->dbname)
		return format_option->dbname;
	mu_error(_("database file name is not given"));
	exit(EX_USAGE);
}

int
convert_rate(const char *arg, double *rate)
{
        double count;
        char *p;

        count = strtod(arg, &p);
        if (*p) {
                time_t div;
                const char *endp;

                while (*p && mu_isspace(*p))
                        p++;
                if (mu_isalpha(*p)) {
                        if (strlen(p) > 3
                            && memcmp(p, "per", 3) == 0 && mu_isspace(p[3]))
                                p += 3;
                } else if (mu_ispunct(*p))
                        p++;

                if (parse_time_interval(p, &div, &endp)) {
                        mu_error(_("unrecognized time format (near `%s')"),
                                 endp);
                        return 1;
                }
                count /= div;
        }
        *rate = count;
        return 0;
}
                

void
mfdbtool_delete(int argc, char **argv)
{
	int i;
	char *name = get_db_name();

	for (i = 0; i < argc; i++)
		db_delete(name, argv[i]);
}

void
mfdbtool_list(int argc, char **argv)
{
	int rc;
	char *name = get_db_name();
	
	if (!format_option->print_item) {
		/* Should not happen */
		mu_error(_("list is not applicable to this DB format"));
		exit(EX_USAGE);
	}		

	rc = 0;
	if (argc) {
		int i;
		for (i = 0; i < argc; i++) 
			rc += db_list_item(name, argv[i],
					   format_option->print_item);
	} else
		rc = db_list(name, format_option->print_item);
	exit(rc != 0);
}

static int
db_proc_enumerator(struct db_format *fmt, void *data)
{
	int (*func)(char *name, db_expire_t exp) = data;
	
	if (fmt->expire) 
		func(fmt->dbname, fmt->expire);
	return 0;
}

void
mfdbtool_expire(int argc, char **argv)
{
	if (all_option) 
		db_format_enumerate(db_proc_enumerator, db_expire);
	else {
		if (!format_option->expire) {
			mu_error(_("expire is not applicable to this DB format"));
			exit(EX_USAGE);
		}		
		exit(db_expire(get_db_name(), format_option->expire) != 0);
	}
}	

void
mfdbtool_compact(int argc, char **argv)
{
	if (all_option) 
		db_format_enumerate(db_proc_enumerator, db_compact);
	else {
		if (!format_option->expire) {
			mu_error(_("compact is not applicable to this DB format"));
			exit(EX_USAGE);
		}
		exit(db_compact(get_db_name(), format_option->expire) != 0);
	}
}


const char *program_version = "mfdbtool (" PACKAGE_STRING ")";
static char prog_doc[] = N_("mfdbtool -- Mailfromd database management tool");

static void
opt_delete(struct mu_parseopt *po, struct mu_option *opt, char const *arg)
{
	run = mfdbtool_delete;
}

static void
opt_list(struct mu_parseopt *po, struct mu_option *opt, char const *arg)
{
	run = mfdbtool_list;
}

static void
opt_expire(struct mu_parseopt *po, struct mu_option *opt, char const *arg)
{
	run = mfdbtool_expire;
}

static void
opt_compact(struct mu_parseopt *po, struct mu_option *opt, char const *arg)
{
	run = mfdbtool_compact;
}

static void
opt_format(struct mu_parseopt *po, struct mu_option *opt, char const *arg)
{
	format_option = db_format_lookup(arg);
	if (format_option == NULL) {
		mu_parseopt_error(po, _("unknown database format: %s"), arg);
		exit(po->po_exit_error);
	}
}

static void
opt_predict(struct mu_parseopt *po, struct mu_option *opt, char const *arg)
{
	if (convert_rate(arg, &predict_rate) == 0) {
		format_option = db_format_lookup("rate");
		predict_next_option = 1;
		run = mfdbtool_list;
	}
}

static void
opt_debug(struct mu_parseopt *po, struct mu_option *opt, char const *arg)
{
	mu_debug_parse_spec(arg);
}

static void
opt_expire_interval(struct mu_parseopt *po, struct mu_option *op,
		    char const *arg)
{
	time_t interval;
	const char *endp;
	if (parse_time_interval(arg, &interval, &endp)) {
		mu_parseopt_error(po,
				  _("unrecognized time format (near `%s')"),
				  endp);
		exit(po->po_exit_error);
	}
	expire_interval = interval;
}

static struct mu_option mfdbtool_options[] = {
	MU_OPTION_GROUP(N_("Database management commands")),
	{ "delete", 0, NULL, MU_OPTION_DEFAULT,
	  N_("delete given entries from the database"),
	  mu_c_string, NULL, opt_delete },
	{ "list", 0, NULL, MU_OPTION_DEFAULT,
	  N_("list given database (default cache)"),
	  mu_c_string, NULL, opt_list },
	{ "expire", 0, NULL, MU_OPTION_DEFAULT,
	  N_("delete expired entries from the database"),
	  mu_c_string, NULL, opt_expire },
	{ "compact", 0, NULL, MU_OPTION_DEFAULT,
	  N_("compact the database"),
	  mu_c_string, NULL, opt_compact },

	MU_OPTION_GROUP(N_("Command modifiers")),
	{ "file", 'f', N_("FILE"), MU_OPTION_DEFAULT,
	  N_("DB file name to operate upon"),
	  mu_c_string, &file_option },
	{ "format", 'H', N_("FORMAT"), MU_OPTION_DEFAULT,
	  N_("format of the DB file to operate upon"),
	  mu_c_string, NULL, opt_format },
	{ "ignore-failed-reads", 0, NULL, MU_OPTION_DEFAULT,
	  N_("ignore failed reads while compacting the database"),
	  mu_c_bool, &ignore_failed_reads_option },
	{ "predict", 0, N_("RATE"), MU_OPTION_DEFAULT,
	  N_("predict when the user will be able to send next message"),
	  mu_c_string, NULL, opt_predict },
	{ "all", 0, NULL, MU_OPTION_DEFAULT,
	  N_("with --compact or --expire: apply the operation to all "
	     "available databases"),
	  mu_c_bool, &all_option },
	/* FIXME
	{ "time-format", 0, N_("FMT"), MU_OPTION_DEFAULT,
	  N_("output timestamps using given format (default \"%c\")"),
	},
	*/
	{ "debug", 'd', N_("LEVEL"), MU_OPTION_DEFAULT,
	  N_("set debugging level"),
	  mu_c_string, NULL, opt_debug },
	
	MU_OPTION_GROUP(N_("Configuration modifiers")),
	{ "state-directory", 0, N_("DIR"), MU_OPTION_DEFAULT,
	  N_("set new program state directory"),
	  mu_c_string, &state_dir },
	{ "expire-interval", 'e', N_("NUMBER"), MU_OPTION_DEFAULT,
	  N_("set database expiration interval to NUMBER seconds"),
	  mu_c_string, NULL, opt_expire_interval },
	MU_OPTION_END
}, *options[] = { mfdbtool_options, NULL };

static int
db_proc_set_expire(struct db_format *fmt, void *data)
{
	fmt->expire_interval = expire_interval;
	return 0;
}

static int
cb_debug(void *data, mu_config_value_t *arg)
{
	if (mu_cfg_assert_value_type(arg, MU_CFG_STRING))
		return 1;
	mu_debug_parse_spec(arg->v.string);
	return 0;
}

struct mu_cfg_param mfdbtool_cfg_param[] = {
	{ "database-type", mu_c_string, &db_type_str, 0, NULL,
	  N_("Default database type"),
	  N_("type") },
	{ "database", mu_cfg_section, NULL, 0, NULL, NULL },
	{ "database-mode", mu_cfg_callback,
	  &mf_database_mode, 0, cb_database_mode,
	  N_("Configure file mode for database files"),
	  N_("mode: octal") },
	{ "state-directory", mu_c_string, &state_dir, 0, NULL,
	  N_("Set program state directory."),
	  N_("dir") },
	{ "debug", mu_cfg_callback, NULL, 0, cb_debug,
	  N_("Set mailfromd debugging level. Argument is a semicolon-separated list "
	     "of debugging specifications. A simplified specification syntax is:\n"
	     "   [!]<category: string>[.<level: string>,<level: string>...]\n"
	     "For details, please see the Mailfromd manual, section 3.18 \"Logging\n"
	     "and Debugging\", GNU Mailutils manual, section 3.3 \"Debugging\",\n"
	     "or visit <http://mailutils.org/wiki/Debug_level>."),
	  N_("spec: list") },
	
	{ NULL }
};

static char *capa[] = {
	"debug",
	"locking",
	NULL
};

struct mu_cli_setup cli = {
	.optv = options,
	.cfg = mfdbtool_cfg_param,
	.prog_doc = prog_doc,
};

void
alloc_die_fun(void)
{
	mu_error(_("not enough memory"));
        abort();
}

int
main(int argc, char **argv)
{
	int rc;
	
	mf_init_nls();
	mu_alloc_die_hook = alloc_die_fun;

	db_format_setup();
	database_cfg_init();
	mf_getopt(&cli, &argc, &argv, capa, MF_GETOPT_DEFAULT);
	
	if (db_type_str) {
		mu_url_t dbhint;

		if ((rc = mu_url_create_null(&dbhint)) ||
		    (rc = mu_url_set_scheme(dbhint, db_type_str))) {
			mu_error(_("cannot initialize DBM hint: %s"),
				 mu_strerror(rc));
			exit(EX_SOFTWARE);
		}
		mu_url_destroy(&mu_dbm_hint);
		mu_dbm_hint = dbhint;
	}

	if (!run) {
		mu_error(_("you must specify one of --list, --delete,"
			   " --expire or --compat"));
		exit(EX_USAGE);
	}
	if (all_option &&
	    !(run == mfdbtool_compact || run == mfdbtool_expire)) {
		mu_error(_("--all is meaningful only with "
			   "--expire or --compact option"));
		exit(EX_USAGE);
	}		
	if (all_option && format_option) {
		mu_error(_("--format is incompatible with --all"));
		exit(EX_USAGE);
	}
	if (!format_option)
		format_option = db_format_lookup("cache");
	if (!format_option) {
		mu_error("INTERNAL ERROR: cache database not defined");
		exit(EX_SOFTWARE);
	}
	if (expire_interval)
		db_format_enumerate(db_proc_set_expire, NULL);

	if (chdir(state_dir)) {
		mu_error(_("cannot change to %s: %s"),
			 state_dir, strerror(errno));
		exit(EX_OSERR);
	}

	run(argc, argv);
	
	exit(0);
}
	
	

