/*
 *  idmap_autorid_tdb: This file contains common code used by
 *  idmap_autorid and net idmap autorid utilities. The common
 *  code provides functions for performing various operations
 *  on autorid.tdb
 *
 *  Copyright (C) Christian Ambach, 2010-2012
 *  Copyright (C) Atul Kulkarni, 2013
 *  Copyright (C) Michael Adam, 2012-2013
 *
 *  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 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.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, see <http://www.gnu.org/licenses/>.
 *
 */

#include "idmap_autorid_tdb.h"
#include "../libcli/security/dom_sid.h"
#include "lib/util/string_wrappers.h"

/**
 * Build the database keystring for getting a range
 * belonging to a domain sid and a range index.
 */
static void idmap_autorid_build_keystr(const char *domsid,
				       uint32_t domain_range_index,
				       fstring keystr)
{
	if (domain_range_index > 0) {
		fstr_sprintf(keystr, "%s#%"PRIu32,
			     domsid, domain_range_index);
	} else {
		fstrcpy(keystr, domsid);
	}
}

static char *idmap_autorid_build_keystr_talloc(TALLOC_CTX *mem_ctx,
					      const char *domsid,
					      uint32_t domain_range_index)
{
	char *keystr;

	if (domain_range_index > 0) {
		keystr = talloc_asprintf(mem_ctx, "%s#%"PRIu32, domsid,
					 domain_range_index);
	} else {
		keystr = talloc_strdup(mem_ctx, domsid);
	}

	return keystr;
}


static bool idmap_autorid_validate_sid(const char *sid)
{
	struct dom_sid ignore;
	if (sid == NULL) {
		return false;
	}

	if (strcmp(sid, ALLOC_RANGE) == 0) {
		return true;
	}

	return dom_sid_parse(sid, &ignore);
}

struct idmap_autorid_addrange_ctx {
	struct autorid_range_config *range;
	bool acquire;
};

static NTSTATUS idmap_autorid_addrange_action(struct db_context *db,
					      void *private_data)
{
	struct idmap_autorid_addrange_ctx *ctx;
	uint32_t requested_rangenum, stored_rangenum;
	struct autorid_range_config *range;
	bool acquire;
	NTSTATUS ret;
	uint32_t hwm;
	char *numstr;
	struct autorid_global_config globalcfg = {0};
	fstring keystr;
	uint32_t increment;
	TALLOC_CTX *mem_ctx = NULL;

	ctx = (struct idmap_autorid_addrange_ctx *)private_data;
	range = ctx->range;
	acquire = ctx->acquire;

	if (db == NULL) {
		DEBUG(3, ("Invalid database argument: NULL\n"));
		return NT_STATUS_INVALID_PARAMETER;
	}

	if (range == NULL) {
		DEBUG(3, ("Invalid range argument: NULL\n"));
		return NT_STATUS_INVALID_PARAMETER;
	}

	requested_rangenum = range->rangenum;

	DEBUG(10, ("Adding new range for domain %s "
		   "(domain_range_index=%"PRIu32")\n",
		   range->domsid, range->domain_range_index));

	if (!idmap_autorid_validate_sid(range->domsid)) {
		DEBUG(3, ("Invalid SID: %s\n", range->domsid));
		return NT_STATUS_INVALID_PARAMETER;
	}

	idmap_autorid_build_keystr(range->domsid, range->domain_range_index,
				   keystr);

	ret = dbwrap_fetch_uint32_bystring(db, keystr, &stored_rangenum);

	if (NT_STATUS_IS_OK(ret)) {
		/* entry is already present*/
		if (acquire) {
			DEBUG(10, ("domain range already allocated - "
				   "Not adding!\n"));

			ret = idmap_autorid_loadconfig(db, &globalcfg);
			if (!NT_STATUS_IS_OK(ret)) {
				DEBUG(1, ("Fatal error while fetching "
					  "configuration: %s\n",
					  nt_errstr(ret)));
				goto error;
			}

			range->rangenum = stored_rangenum;
			range->low_id = globalcfg.minvalue
				+ range->rangenum * globalcfg.rangesize;
			range->high_id =
				range->low_id  + globalcfg.rangesize - 1;

			return NT_STATUS_OK;
		}

		if (stored_rangenum != requested_rangenum) {
			DEBUG(1, ("Error: requested rangenumber (%u) differs "
				  "from stored one (%u).\n",
				  requested_rangenum, stored_rangenum));
			return NT_STATUS_UNSUCCESSFUL;
		}

		DEBUG(10, ("Note: stored range agrees with requested "
			   "one - ok\n"));
		return NT_STATUS_OK;
	}

	/* fetch the current HWM */
	ret = dbwrap_fetch_uint32_bystring(db, HWM, &hwm);
	if (!NT_STATUS_IS_OK(ret)) {
		DEBUG(1, ("Fatal error while fetching current "
			  "HWM value: %s\n", nt_errstr(ret)));
		return NT_STATUS_INTERNAL_ERROR;
	}

	mem_ctx = talloc_stackframe();

	ret = idmap_autorid_loadconfig(db, &globalcfg);
	if (!NT_STATUS_IS_OK(ret)) {
		DEBUG(1, ("Fatal error while fetching configuration: %s\n",
			  nt_errstr(ret)));
		goto error;
	}

	if (acquire) {
		/*
		 * automatically acquire the next range
		 */
		requested_rangenum = hwm;
	}

	if (requested_rangenum >= globalcfg.maxranges) {
		DBG_WARNING("Not enough ranges available: New range %u can't "
			    "be allocated. Consider increasing the range "
			    "[%u-%u] by %u.\n",
			   requested_rangenum,
			   globalcfg.minvalue,
			   globalcfg.minvalue +
				(globalcfg.maxranges * globalcfg.rangesize),
			   globalcfg.rangesize);
		ret = NT_STATUS_NO_MEMORY;
		goto error;
	}

	/*
	 * Check that it is not yet taken.
	 * If the range is requested and < HWM, we need
	 * to check anyways, and otherwise, we also better
	 * check in order to prevent further corruption
	 * in case the db has been externally modified.
	 */

	numstr = talloc_asprintf(mem_ctx, "%u", requested_rangenum);
	if (!numstr) {
		DEBUG(1, ("Talloc failed!\n"));
		ret = NT_STATUS_NO_MEMORY;
		goto error;
	}

	if (dbwrap_exists(db, string_term_tdb_data(numstr))) {
		DEBUG(1, ("Requested range '%s' is already in use.\n", numstr));

		if (requested_rangenum < hwm) {
			ret = NT_STATUS_INVALID_PARAMETER;
		} else {
			ret = NT_STATUS_INTERNAL_DB_CORRUPTION;
		}

		goto error;
	}

	if (requested_rangenum >= hwm) {
		/*
		 * requested or automatic range >= HWM:
		 * increment the HWM.
		 */

		/* HWM always contains current max range + 1 */
		increment = requested_rangenum + 1 - hwm;

		/* increase the HWM */
		ret = dbwrap_change_uint32_atomic_bystring(db, HWM, &hwm,
							   increment);
		if (!NT_STATUS_IS_OK(ret)) {
			DEBUG(1, ("Fatal error while incrementing the HWM "
				  "value in the database: %s\n",
				  nt_errstr(ret)));
			goto error;
		}
	}

	/*
	 * store away the new mapping in both directions
	 */

	ret = dbwrap_store_uint32_bystring(db, keystr, requested_rangenum);
	if (!NT_STATUS_IS_OK(ret)) {
		DEBUG(1, ("Fatal error while storing new "
			  "domain->range assignment: %s\n", nt_errstr(ret)));
		goto error;
	}

	numstr = talloc_asprintf(mem_ctx, "%u", requested_rangenum);
	if (!numstr) {
		ret = NT_STATUS_NO_MEMORY;
		goto error;
	}

	ret = dbwrap_store_bystring(db, numstr,
			string_term_tdb_data(keystr), TDB_INSERT);

	if (!NT_STATUS_IS_OK(ret)) {
		DEBUG(1, ("Fatal error while storing new "
			  "domain->range assignment: %s\n", nt_errstr(ret)));
		goto error;
	}

	DEBUG(5, ("%s new range #%d for domain %s "
		  "(domain_range_index=%"PRIu32")\n",
		  (acquire?"Acquired":"Stored"),
		  requested_rangenum, keystr,
		  range->domain_range_index));

	range->rangenum = requested_rangenum;

	range->low_id = globalcfg.minvalue
		      + range->rangenum * globalcfg.rangesize;
	range->high_id = range->low_id  + globalcfg.rangesize - 1;

	ret = NT_STATUS_OK;

error:
	talloc_free(mem_ctx);
	return ret;
}

static NTSTATUS idmap_autorid_addrange(struct db_context *db,
				       struct autorid_range_config *range,
				       bool acquire)
{
	NTSTATUS status;
	struct idmap_autorid_addrange_ctx ctx;

	ctx.acquire = acquire;
	ctx.range = range;

	status = dbwrap_trans_do(db, idmap_autorid_addrange_action, &ctx);
	return status;
}

NTSTATUS idmap_autorid_setrange(struct db_context *db,
				const char *domsid,
				uint32_t domain_range_index,
				uint32_t rangenum)
{
	NTSTATUS status;
	struct autorid_range_config range;

	ZERO_STRUCT(range);
	fstrcpy(range.domsid, domsid);
	range.domain_range_index = domain_range_index;
	range.rangenum = rangenum;

	status = idmap_autorid_addrange(db, &range, false);
	return status;
}

NTSTATUS idmap_autorid_acquire_range(struct db_context *db,
				     struct autorid_range_config *range)
{
	return idmap_autorid_addrange(db, range, true);
}

static NTSTATUS idmap_autorid_getrange_int(struct db_context *db,
					   struct autorid_range_config *range)
{
	NTSTATUS status = NT_STATUS_INVALID_PARAMETER;
	struct autorid_global_config globalcfg = {0};
	fstring keystr;

	if (db == NULL || range == NULL) {
		DEBUG(3, ("Invalid arguments received\n"));
		goto done;
	}

	if (!idmap_autorid_validate_sid(range->domsid)) {
		DEBUG(3, ("Invalid SID: '%s'\n", range->domsid));
		status = NT_STATUS_INVALID_PARAMETER;
		goto done;
	}

	idmap_autorid_build_keystr(range->domsid, range->domain_range_index,
				   keystr);

	DEBUG(10, ("reading domain range for key %s\n", keystr));
	status = dbwrap_fetch_uint32_bystring(db, keystr, &(range->rangenum));
	if (!NT_STATUS_IS_OK(status)) {
		DEBUG(1, ("Failed to read database record for key '%s': %s\n",
			  keystr, nt_errstr(status)));
		goto done;
	}

	status = idmap_autorid_loadconfig(db, &globalcfg);
	if (!NT_STATUS_IS_OK(status)) {
		DEBUG(1, ("Failed to read global configuration\n"));
		goto done;
	}
	range->low_id = globalcfg.minvalue
		      + range->rangenum * globalcfg.rangesize;
	range->high_id = range->low_id  + globalcfg.rangesize - 1;
done:
	return status;
}

NTSTATUS idmap_autorid_getrange(struct db_context *db,
				const char *domsid,
				uint32_t domain_range_index,
				uint32_t *rangenum,
				uint32_t *low_id)
{
	NTSTATUS status;
	struct autorid_range_config range;

	if (rangenum == NULL) {
		return NT_STATUS_INVALID_PARAMETER;
	}

	ZERO_STRUCT(range);
	fstrcpy(range.domsid, domsid);
	range.domain_range_index = domain_range_index;

	status = idmap_autorid_getrange_int(db, &range);
	if (!NT_STATUS_IS_OK(status)) {
		return status;
	}

	*rangenum = range.rangenum;

	if (low_id != NULL) {
		*low_id = range.low_id;
	}

	return NT_STATUS_OK;
}

NTSTATUS idmap_autorid_get_domainrange(struct db_context *db,
				       struct autorid_range_config *range,
				       bool read_only)
{
	NTSTATUS ret;

	ret = idmap_autorid_getrange_int(db, range);
	if (!NT_STATUS_IS_OK(ret)) {
		DEBUG(10, ("Failed to read range config for '%s': %s\n",
			   range->domsid, nt_errstr(ret)));
		if (read_only) {
			DEBUG(10, ("Not allocating new range for '%s' because "
				   "read-only is enabled.\n", range->domsid));
			return NT_STATUS_NOT_FOUND;
		}

		ret = idmap_autorid_acquire_range(db, range);
	}

	DEBUG(10, ("Using range #%d for domain %s "
		   "(domain_range_index=%"PRIu32", low_id=%"PRIu32")\n",
		   range->rangenum, range->domsid, range->domain_range_index,
		   range->low_id));

	return ret;
}

/* initialize the given HWM to 0 if it does not exist yet */
static NTSTATUS idmap_autorid_init_hwm_action(struct db_context *db,
					      void *private_data)
{
	NTSTATUS status;
	uint32_t hwmval;
	const char *hwm;

	hwm = (char *)private_data;

	status = dbwrap_fetch_uint32_bystring(db, hwm, &hwmval);
	if (NT_STATUS_IS_OK(status)) {
		DEBUG(1, ("HWM (%s) already initialized in autorid database "
			  "(value %"PRIu32").\n", hwm, hwmval));
		return NT_STATUS_OK;
	}
	if (!NT_STATUS_EQUAL(status, NT_STATUS_NOT_FOUND)) {
		DEBUG(0, ("Error fetching HWM (%s) from autorid "
			  "database: %s\n", hwm, nt_errstr(status)));
		return status;
	}

	status = dbwrap_trans_store_uint32_bystring(db, hwm, 0);
	if (!NT_STATUS_IS_OK(status)) {
		DEBUG(0, ("Error storing HWM (%s) in autorid database: %s\n",
			  hwm, nt_errstr(status)));
		return status;
	}

	return NT_STATUS_OK;
}

NTSTATUS idmap_autorid_init_hwm(struct db_context *db, const char *hwm)
{
	NTSTATUS status;
	uint32_t hwmval;

	status = dbwrap_fetch_uint32_bystring(db, hwm, &hwmval);
	if (NT_STATUS_IS_OK(status)) {
		DEBUG(1, ("HWM (%s) already initialized in autorid database "
			  "(value %"PRIu32").\n", hwm, hwmval));
		return NT_STATUS_OK;
	}
	if (!NT_STATUS_EQUAL(status, NT_STATUS_NOT_FOUND)) {
		DEBUG(0, ("unable to fetch HWM (%s) from autorid "
			  "database: %s\n", hwm,  nt_errstr(status)));
		return status;
	}

	status = dbwrap_trans_do(db, idmap_autorid_init_hwm_action,
				 discard_const(hwm));
	if (!NT_STATUS_IS_OK(status)) {
		DEBUG(0, ("Error initializing HWM (%s) in autorid database: "
			  "%s\n", hwm, nt_errstr(status)));
		return NT_STATUS_INTERNAL_DB_ERROR;
	}

	DEBUG(1, ("Initialized HWM (%s) in autorid database.\n", hwm));

	return NT_STATUS_OK;
}

/*
 * Delete a domain#index <-> range mapping from the database.
 * The mapping is specified by the sid and index.
 * If force == true, invalid mapping records are deleted as far
 * as possible, otherwise they are left untouched.
 */

struct idmap_autorid_delete_range_by_sid_ctx {
	const char *domsid;
	uint32_t domain_range_index;
	bool force;
};

static NTSTATUS idmap_autorid_delete_range_by_sid_action(struct db_context *db,
							 void *private_data)
{
	struct idmap_autorid_delete_range_by_sid_ctx *ctx =
		(struct idmap_autorid_delete_range_by_sid_ctx *)private_data;
	const char *domsid;
	uint32_t domain_range_index;
	uint32_t rangenum;
	char *keystr;
	char *range_keystr;
	TDB_DATA data;
	NTSTATUS status;
	TALLOC_CTX *frame = talloc_stackframe();
	bool is_valid_range_mapping = true;
	bool force;

	domsid = ctx->domsid;
	domain_range_index = ctx->domain_range_index;
	force = ctx->force;

	keystr = idmap_autorid_build_keystr_talloc(frame, domsid,
						   domain_range_index);
	if (keystr == NULL) {
		status = NT_STATUS_NO_MEMORY;
		goto done;
	}

	status = dbwrap_fetch_uint32_bystring(db, keystr, &rangenum);
	if (!NT_STATUS_IS_OK(status)) {
		goto done;
	}

	range_keystr = talloc_asprintf(frame, "%"PRIu32, rangenum);
	if (range_keystr == NULL) {
		status = NT_STATUS_NO_MEMORY;
		goto done;
	}

	status = dbwrap_fetch_bystring(db, frame, range_keystr, &data);
	if (NT_STATUS_EQUAL(status, NT_STATUS_NOT_FOUND)) {
		DEBUG(1, ("Incomplete mapping %s -> %s: no backward mapping\n",
			  keystr, range_keystr));
		is_valid_range_mapping = false;
	} else if (!NT_STATUS_IS_OK(status)) {
		DEBUG(1, ("Error fetching reverse mapping for %s -> %s:  %s\n",
			  keystr, range_keystr, nt_errstr(status)));
		goto done;
	} else if (strncmp((const char *)data.dptr, keystr, strlen(keystr))
		   != 0)
	{
		DEBUG(1, ("Invalid mapping: %s -> %s -> %s\n",
			  keystr, range_keystr, (const char *)data.dptr));
		is_valid_range_mapping = false;
	}

	if (!is_valid_range_mapping && !force) {
		DEBUG(10, ("Not deleting invalid mapping, since not in force "
			   "mode.\n"));
		status = NT_STATUS_FILE_INVALID;
		goto done;
	}

	status = dbwrap_delete_bystring(db, keystr);
	if (!NT_STATUS_IS_OK(status)) {
		DEBUG(1, ("Deletion of '%s' failed: %s\n",
			  keystr, nt_errstr(status)));
		goto done;
	}

	if (!is_valid_range_mapping) {
		goto done;
	}

	status = dbwrap_delete_bystring(db, range_keystr);
	if (!NT_STATUS_IS_OK(status)) {
		DEBUG(1, ("Deletion of '%s' failed: %s\n",
			  range_keystr, nt_errstr(status)));
		goto done;
	}

	DEBUG(10, ("Deleted range mapping %s <--> %s\n", keystr,
		   range_keystr));

done:
	TALLOC_FREE(frame);
	return status;
}

NTSTATUS idmap_autorid_delete_range_by_sid(struct db_context *db,
					   const char *domsid,
					   uint32_t domain_range_index,
					   bool force)
{
	NTSTATUS status;
	struct idmap_autorid_delete_range_by_sid_ctx ctx;

	ctx.domain_range_index = domain_range_index;
	ctx.domsid = domsid;
	ctx.force = force;

	status = dbwrap_trans_do(db, idmap_autorid_delete_range_by_sid_action,
				 &ctx);
	return status;
}

/*
 * Delete a domain#index <-> range mapping from the database.
 * The mapping is specified by the range number.
 * If force == true, invalid mapping records are deleted as far
 * as possible, otherwise they are left untouched.
 */
struct idmap_autorid_delete_range_by_num_ctx {
	uint32_t rangenum;
	bool force;
};

static NTSTATUS idmap_autorid_delete_range_by_num_action(struct db_context *db,
							   void *private_data)
{
	struct idmap_autorid_delete_range_by_num_ctx *ctx =
		(struct idmap_autorid_delete_range_by_num_ctx *)private_data;
	uint32_t rangenum;
	char *keystr = NULL;
	char *range_keystr;
	TDB_DATA val;
	NTSTATUS status;
	TALLOC_CTX *frame = talloc_stackframe();
	bool is_valid_range_mapping = true;
	bool force;

	rangenum = ctx->rangenum;
	force = ctx->force;

	range_keystr = talloc_asprintf(frame, "%"PRIu32, rangenum);
	if (range_keystr == NULL) {
		status = NT_STATUS_NO_MEMORY;
		goto done;
	}

	ZERO_STRUCT(val);

	status = dbwrap_fetch_bystring(db, frame, range_keystr, &val);
	if (NT_STATUS_EQUAL(status, NT_STATUS_NOT_FOUND)) {
		DEBUG(10, ("Did not find range '%s' in database.\n",
			   range_keystr));
		goto done;
	} else if (!NT_STATUS_IS_OK(status)) {
		DEBUG(5, ("Error fetching rang key: %s\n", nt_errstr(status)));
		goto done;
	}

	if (val.dptr == NULL) {
		DEBUG(1, ("Invalid mapping: %s -> empty value\n",
			  range_keystr));
		is_valid_range_mapping = false;
	} else {
		uint32_t reverse_rangenum = 0;

		keystr = (char *)val.dptr;

		status = dbwrap_fetch_uint32_bystring(db, keystr,
						      &reverse_rangenum);
		if (NT_STATUS_EQUAL(status, NT_STATUS_NOT_FOUND)) {
			DEBUG(1, ("Incomplete mapping %s -> %s: "
				  "no backward mapping\n",
				  range_keystr, keystr));
			is_valid_range_mapping = false;
		} else if (!NT_STATUS_IS_OK(status)) {
			DEBUG(1, ("Error fetching reverse mapping for "
				  "%s -> %s: %s\n",
				  range_keystr, keystr, nt_errstr(status)));
			goto done;
		} else if (rangenum != reverse_rangenum) {
			is_valid_range_mapping = false;
		}
	}

	if (!is_valid_range_mapping && !force) {
		DEBUG(10, ("Not deleting invalid mapping, since not in force "
			   "mode.\n"));
		status = NT_STATUS_FILE_INVALID;
		goto done;
	}

	status = dbwrap_delete_bystring(db, range_keystr);
	if (!NT_STATUS_IS_OK(status)) {
		DEBUG(1, ("Deletion of '%s' failed: %s\n",
			  range_keystr, nt_errstr(status)));
		goto done;
	}

	if (!is_valid_range_mapping) {
		goto done;
	}

	status = dbwrap_delete_bystring(db, keystr);
	if (!NT_STATUS_IS_OK(status)) {
		DEBUG(1, ("Deletion of '%s' failed: %s\n",
			  keystr, nt_errstr(status)));
		goto done;
	}

	DEBUG(10, ("Deleted range mapping %s <--> %s\n", range_keystr,
		   keystr));

done:
	talloc_free(frame);
	return status;
}

NTSTATUS idmap_autorid_delete_range_by_num(struct db_context *db,
					   uint32_t rangenum,
					   bool force)
{
	NTSTATUS status;
	struct idmap_autorid_delete_range_by_num_ctx ctx;

	ctx.rangenum = rangenum;
	ctx.force = force;

	status = dbwrap_trans_do(db, idmap_autorid_delete_range_by_num_action,
				 &ctx);
	return status;
}

/**
 * Open and possibly create the database.
 */
NTSTATUS idmap_autorid_db_open(const char *path,
			       TALLOC_CTX *mem_ctx,
			       struct db_context **db)
{
	if (*db != NULL) {
		/* its already open */
		return NT_STATUS_OK;
	}

	/* Open idmap repository */
	*db = db_open(mem_ctx, path, 0, TDB_DEFAULT, O_RDWR | O_CREAT, 0644,
		      DBWRAP_LOCK_ORDER_1, DBWRAP_FLAG_NONE);

	if (*db == NULL) {
		DEBUG(0, ("Unable to open idmap_autorid database '%s'\n", path));
		return NT_STATUS_UNSUCCESSFUL;
	}

	return NT_STATUS_OK;
}

/**
 * Initialize the high watermark records in the database.
 */
NTSTATUS idmap_autorid_init_hwms(struct db_context *db)
{
	NTSTATUS status;

	status = idmap_autorid_init_hwm(db, HWM);
	if (!NT_STATUS_IS_OK(status)) {
		return status;
	}

	status = idmap_autorid_init_hwm(db, ALLOC_HWM_UID);
	if (!NT_STATUS_IS_OK(status)) {
		return status;
	}

	status = idmap_autorid_init_hwm(db, ALLOC_HWM_GID);

	return status;
}

NTSTATUS idmap_autorid_db_init(const char *path,
			       TALLOC_CTX *mem_ctx,
			       struct db_context **db)
{
	NTSTATUS status;

	status = idmap_autorid_db_open(path, mem_ctx, db);
	if (!NT_STATUS_IS_OK(status)) {
		return status;
	}

	status = idmap_autorid_init_hwms(*db);
	return status;
}



struct idmap_autorid_fetch_config_state {
	TALLOC_CTX *mem_ctx;
	char *configstr;
};

static void idmap_autorid_config_parser(TDB_DATA key, TDB_DATA value,
					void *private_data)
{
	struct idmap_autorid_fetch_config_state *state;

	state = (struct idmap_autorid_fetch_config_state *)private_data;

	/*
	 * strndup because we have non-nullterminated strings in the db
	 */
	state->configstr = talloc_strndup(
		state->mem_ctx, (const char *)value.dptr, value.dsize);
}

NTSTATUS idmap_autorid_getconfigstr(struct db_context *db, TALLOC_CTX *mem_ctx,
				    char **result)
{
	TDB_DATA key;
	NTSTATUS status;
	struct idmap_autorid_fetch_config_state state;

	if (result == NULL) {
		return NT_STATUS_INVALID_PARAMETER;
	}

	key = string_term_tdb_data(CONFIGKEY);

	state.mem_ctx = mem_ctx;
	state.configstr = NULL;

	status = dbwrap_parse_record(db, key, idmap_autorid_config_parser,
				     &state);
	if (!NT_STATUS_IS_OK(status)) {
		DEBUG(1, ("Error while retrieving config: %s\n",
			  nt_errstr(status)));
		return status;
	}

	if (state.configstr == NULL) {
		DEBUG(1, ("Error while retrieving config\n"));
		return NT_STATUS_NO_MEMORY;
	}

	DEBUG(5, ("found CONFIG: %s\n", state.configstr));

	*result = state.configstr;
	return NT_STATUS_OK;
}

bool idmap_autorid_parse_configstr(const char *configstr,
				   struct autorid_global_config *cfg)
{
	unsigned long minvalue, rangesize, maxranges;

	if (sscanf(configstr,
		   "minvalue:%lu rangesize:%lu maxranges:%lu",
		   &minvalue, &rangesize, &maxranges) != 3) {
		DEBUG(1,
		      ("Found invalid configuration data. "
		       "Creating new config\n"));
		return false;
	}

	cfg->minvalue = minvalue;
	cfg->rangesize = rangesize;
	cfg->maxranges = maxranges;

	return true;
}

NTSTATUS idmap_autorid_loadconfig(struct db_context *db,
				  struct autorid_global_config *result)
{
	struct autorid_global_config cfg = {0};
	NTSTATUS status;
	bool ok;
	char *configstr = NULL;

	if (result == NULL) {
		return NT_STATUS_INVALID_PARAMETER;
	}

	status = idmap_autorid_getconfigstr(db, db, &configstr);
	if (!NT_STATUS_IS_OK(status)) {
		return status;
	}

	ok = idmap_autorid_parse_configstr(configstr, &cfg);
	TALLOC_FREE(configstr);
	if (!ok) {
		return NT_STATUS_INVALID_PARAMETER;
	}

	DEBUG(10, ("Loaded previously stored configuration "
		   "minvalue:%d rangesize:%d\n",
		   cfg.minvalue, cfg.rangesize));

	*result = cfg;

	return NT_STATUS_OK;
}

NTSTATUS idmap_autorid_saveconfig(struct db_context *db,
				  struct autorid_global_config *cfg)
{

	struct autorid_global_config storedconfig = {0};
	NTSTATUS status = NT_STATUS_INVALID_PARAMETER;
	TDB_DATA data;
	char *cfgstr;
	uint32_t hwm;
	TALLOC_CTX *frame = talloc_stackframe();

	DEBUG(10, ("New configuration provided for storing is "
		   "minvalue:%d rangesize:%d maxranges:%d\n",
		   cfg->minvalue, cfg->rangesize, cfg->maxranges));

	if (cfg->rangesize < 2000) {
		DEBUG(1, ("autorid rangesize must be at least 2000\n"));
		goto done;
	}

	if (cfg->maxranges == 0) {
		DEBUG(1, ("An autorid maxranges value of 0 is invalid. "
			  "Must have at least one range available.\n"));
		goto done;
	}

	status = idmap_autorid_loadconfig(db, &storedconfig);
	if (NT_STATUS_EQUAL(status, NT_STATUS_NOT_FOUND)) {
		DEBUG(5, ("No configuration found. Storing initial "
			  "configuration.\n"));
		storedconfig = *cfg;
	} else if (!NT_STATUS_IS_OK(status)) {
		DEBUG(1, ("Error loading configuration: %s\n",
			  nt_errstr(status)));
		goto done;
	}

	/* did the minimum value or rangesize change? */
	if ((storedconfig.minvalue != cfg->minvalue) ||
	    (storedconfig.rangesize != cfg->rangesize))
	{
		DEBUG(1, ("New configuration values for rangesize or "
			  "minimum uid value conflict with previously "
			  "used values! Not storing new config.\n"));
		status = NT_STATUS_INVALID_PARAMETER;
		goto done;
	}

	status = dbwrap_fetch_uint32_bystring(db, HWM, &hwm);
	if (!NT_STATUS_IS_OK(status)) {
		DEBUG(1, ("Fatal error while fetching current "
			  "HWM value: %s\n", nt_errstr(status)));
		status = NT_STATUS_INTERNAL_ERROR;
		goto done;
	}

	/*
	 * has the highest uid value been reduced to setting that is not
	 * sufficient any more for already existing ranges?
	 */
	if (hwm > cfg->maxranges) {
		DEBUG(1, ("New upper uid limit is too low to cover "
			  "existing mappings! Not storing new config.\n"));
		status = NT_STATUS_INVALID_PARAMETER;
		goto done;
	}

	cfgstr =
	    talloc_asprintf(frame,
			    "minvalue:%u rangesize:%u maxranges:%u",
			    cfg->minvalue, cfg->rangesize, cfg->maxranges);

	if (cfgstr == NULL) {
		status = NT_STATUS_NO_MEMORY;
		goto done;
	}

	data = string_tdb_data(cfgstr);

	status = dbwrap_trans_store_bystring(db, CONFIGKEY, data, TDB_REPLACE);

done:
	TALLOC_FREE(frame);
	return status;
}

NTSTATUS idmap_autorid_saveconfigstr(struct db_context *db,
				     const char *configstr)
{
	bool ok;
	NTSTATUS status;
	struct autorid_global_config cfg;

	ok = idmap_autorid_parse_configstr(configstr, &cfg);
	if (!ok) {
		return NT_STATUS_INVALID_PARAMETER;
	}

	status = idmap_autorid_saveconfig(db, &cfg);
	return status;
}


/*
 * iteration: Work on all range mappings for a given domain
 */

struct domain_range_visitor_ctx {
	const char *domsid;
	NTSTATUS (*fn)(struct db_context *db,
		       const char *domsid,
		       uint32_t index,
		       uint32_t rangenum,
		       void *private_data);
	void *private_data;
	int count; /* number of records worked on */
};

static int idmap_autorid_visit_domain_range(struct db_record *rec,
					    void *private_data)
{
	struct domain_range_visitor_ctx *vi;
	char *domsid;
	char *sep;
	uint32_t range_index = 0;
	uint32_t rangenum = 0;
	TDB_DATA key, value;
	NTSTATUS status;
	int ret = 0;
	struct db_context *db;

	vi = talloc_get_type_abort(private_data,
				   struct domain_range_visitor_ctx);

	key = dbwrap_record_get_key(rec);

	/*
	 * split string "<sid>[#<index>]" into sid string and index number
	 */

	domsid = (char *)key.dptr;

	DEBUG(10, ("idmap_autorid_visit_domain_range: visiting key '%s'\n",
		   domsid));

	sep = strrchr(domsid, '#');
	if (sep != NULL) {
		char *index_str;
		*sep = '\0';
		index_str = sep+1;
		if (sscanf(index_str, "%"SCNu32, &range_index) != 1) {
			DEBUG(10, ("Found separator '#' but '%s' is not a "
				   "valid range index. Skipping record\n",
				   index_str));
			goto done;
		}
	}

	if (!idmap_autorid_validate_sid(domsid)) {
		DEBUG(10, ("String '%s' is not a valid sid. "
			   "Skipping record.\n", domsid));
		goto done;
	}

	if ((vi->domsid != NULL) && (strcmp(domsid, vi->domsid) != 0)) {
		DEBUG(10, ("key sid '%s' does not match requested sid '%s'.\n",
			   domsid, vi->domsid));
		goto done;
	}

	value = dbwrap_record_get_value(rec);

	if (value.dsize != sizeof(uint32_t)) {
		/* it might be a mapping of a well known sid */
		DEBUG(10, ("value size %u != sizeof(uint32_t) for sid '%s', "
			   "skipping.\n", (unsigned)value.dsize, vi->domsid));
		goto done;
	}

	rangenum = IVAL(value.dptr, 0);

	db = dbwrap_record_get_db(rec);

	status = vi->fn(db, domsid, range_index, rangenum, vi->private_data);
	if (!NT_STATUS_IS_OK(status)) {
		ret = -1;
		goto done;
	}

	vi->count++;
	ret = 0;

done:
	return ret;
}

static NTSTATUS idmap_autorid_iterate_domain_ranges_int(struct db_context *db,
				const char *domsid,
				NTSTATUS (*fn)(struct db_context *db,
					       const char *domsid,
					       uint32_t index,
					       uint32_t rangnum,
					       void *private_data),
				void *private_data,
				int *count,
				NTSTATUS (*traverse)(struct db_context *db,
					  int (*f)(struct db_record *, void *),
					  void *private_data,
					  int *count))
{
	NTSTATUS status;
	struct domain_range_visitor_ctx *vi;
	TALLOC_CTX *frame = talloc_stackframe();

	if (domsid == NULL) {
		DEBUG(10, ("No sid provided, operating on all ranges\n"));
	}

	if (fn == NULL) {
		DEBUG(1, ("Error: missing visitor callback\n"));
		status = NT_STATUS_INVALID_PARAMETER;
		goto done;
	}

	vi = talloc_zero(frame, struct domain_range_visitor_ctx);
	if (vi == NULL) {
		status = NT_STATUS_NO_MEMORY;
		goto done;
	}

	vi->domsid = domsid;
	vi->fn = fn;
	vi->private_data = private_data;

	status = traverse(db, idmap_autorid_visit_domain_range, vi, NULL);
	if (!NT_STATUS_IS_OK(status)) {
		goto done;
	}

	if (count != NULL) {
		*count = vi->count;
	}

done:
	talloc_free(frame);
	return status;
}

NTSTATUS idmap_autorid_iterate_domain_ranges(struct db_context *db,
					const char *domsid,
					NTSTATUS (*fn)(struct db_context *db,
						       const char *domsid,
						       uint32_t index,
						       uint32_t rangenum,
						       void *private_data),
					void *private_data,
					int *count)
{
	NTSTATUS status;

	status = idmap_autorid_iterate_domain_ranges_int(db,
							 domsid,
							 fn,
							 private_data,
							 count,
							 dbwrap_traverse);

	return status;
}


NTSTATUS idmap_autorid_iterate_domain_ranges_read(struct db_context *db,
					const char *domsid,
					NTSTATUS (*fn)(struct db_context *db,
						       const char *domsid,
						       uint32_t index,
						       uint32_t rangenum,
						       void *count),
					void *private_data,
					int *count)
{
	NTSTATUS status;

	status = idmap_autorid_iterate_domain_ranges_int(db,
							 domsid,
							 fn,
							 private_data,
							 count,
							 dbwrap_traverse_read);

	return status;
}


/*
 * Delete all ranges configured for a given domain
 */

struct delete_domain_ranges_visitor_ctx {
	bool force;
};

static NTSTATUS idmap_autorid_delete_domain_ranges_visitor(
						struct db_context *db,
						const char *domsid,
						uint32_t domain_range_index,
						uint32_t rangenum,
						void *private_data)
{
	struct delete_domain_ranges_visitor_ctx *ctx;
	NTSTATUS status;

	ctx = (struct delete_domain_ranges_visitor_ctx *)private_data;

	status = idmap_autorid_delete_range_by_sid(
				db, domsid, domain_range_index, ctx->force);
	return status;
}

struct idmap_autorid_delete_domain_ranges_ctx {
	const char *domsid;
	bool force;
	int count; /* output: count records operated on */
};

static NTSTATUS idmap_autorid_delete_domain_ranges_action(struct db_context *db,
							  void *private_data)
{
	struct idmap_autorid_delete_domain_ranges_ctx *ctx;
	struct delete_domain_ranges_visitor_ctx visitor_ctx;
	int count;
	NTSTATUS status;

	ctx = (struct idmap_autorid_delete_domain_ranges_ctx *)private_data;

	ZERO_STRUCT(visitor_ctx);
	visitor_ctx.force = ctx->force;

	status = idmap_autorid_iterate_domain_ranges(db,
				ctx->domsid,
				idmap_autorid_delete_domain_ranges_visitor,
				&visitor_ctx,
				&count);
	if (!NT_STATUS_IS_OK(status)) {
		return status;
	}

	ctx->count = count;

	return NT_STATUS_OK;
}

NTSTATUS idmap_autorid_delete_domain_ranges(struct db_context *db,
					    const char *domsid,
					    bool force,
					    int *count)
{
	NTSTATUS status;
	struct idmap_autorid_delete_domain_ranges_ctx ctx;

	ZERO_STRUCT(ctx);
	ctx.domsid = domsid;
	ctx.force = force;

	status = dbwrap_trans_do(db, idmap_autorid_delete_domain_ranges_action,
				 &ctx);
	if (!NT_STATUS_IS_OK(status)) {
		return status;
	}

	*count = ctx.count;

	return NT_STATUS_OK;
}
