/* 
   Unix SMB/CIFS implementation.
   
   WINS Replication server
   
   Copyright (C) Stefan Metzmacher	2005
   
   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 "includes.h"
#include "librpc/gen_ndr/ndr_winsrepl.h"
#include "wrepl_server/wrepl_server.h"
#include "nbt_server/wins/winsdb.h"
#include <ldb.h>
#include <ldb_errors.h>
#include "system/time.h"
#include "samba/service_task.h"
#include "lib/messaging/irpc.h"
#include "librpc/gen_ndr/ndr_irpc_c.h"
#include "librpc/gen_ndr/ndr_nbt.h"
#include "param/param.h"

const char *wreplsrv_owner_filter(struct wreplsrv_service *service,
				  TALLOC_CTX *mem_ctx,
				  const char *wins_owner)
{
	if (strcmp(wins_owner, service->wins_db->local_owner) == 0) {
		return talloc_asprintf(mem_ctx, "(|(winsOwner=%s)(winsOwner=0.0.0.0))",
				       wins_owner);
	}

	return talloc_asprintf(mem_ctx, "(&(winsOwner=%s)(!(winsOwner=0.0.0.0)))",
			       wins_owner);
}

static NTSTATUS wreplsrv_scavenging_owned_records(struct wreplsrv_service *service, TALLOC_CTX *tmp_mem)
{
	NTSTATUS status;
	struct winsdb_record *rec = NULL;
	struct ldb_result *res = NULL;
	const char *owner_filter;
	const char *filter;
	unsigned int i;
	int ret;
	time_t now = time(NULL);
	const char *now_timestr;
	const char *action;
	const char *old_state=NULL;
	const char *new_state=NULL;
	uint32_t modify_flags;
	bool modify_record;
	bool delete_record;
	bool delete_tombstones;
	struct timeval tombstone_extra_time;
	const char *local_owner = service->wins_db->local_owner;
	bool propagate = lpcfg_parm_bool(service->task->lp_ctx, NULL, "wreplsrv", "propagate name releases", false);

	now_timestr = ldb_timestring(tmp_mem, now);
	NT_STATUS_HAVE_NO_MEMORY(now_timestr);
	owner_filter = wreplsrv_owner_filter(service, tmp_mem, local_owner);
	NT_STATUS_HAVE_NO_MEMORY(owner_filter);
	filter = talloc_asprintf(tmp_mem,
				 "(&%s(objectClass=winsRecord)"
				 "(expireTime<=%s))",
				 owner_filter, now_timestr);
	NT_STATUS_HAVE_NO_MEMORY(filter);
	ret = ldb_search(service->wins_db->ldb, tmp_mem, &res, NULL, LDB_SCOPE_SUBTREE, NULL, "%s", filter);
	if (ret != LDB_SUCCESS) return NT_STATUS_INTERNAL_DB_CORRUPTION;
	DEBUG(10,("WINS scavenging: filter '%s' count %d\n", filter, res->count));

	tombstone_extra_time = timeval_add(&service->startup_time,
					   service->config.tombstone_extra_timeout,
					   0);
	delete_tombstones = timeval_expired(&tombstone_extra_time);

	for (i=0; i < res->count; i++) {
		bool has_replicas = false;

		/*
		 * we pass '0' as 'now' here,
		 * because we want to get the raw timestamps which are in the DB
		 */
		status = winsdb_record(service->wins_db, res->msgs[i], tmp_mem, 0, &rec);
		NT_STATUS_NOT_OK_RETURN(status);
		talloc_free(res->msgs[i]);

		modify_flags	= 0;
		modify_record	= false;
		delete_record	= false;

		switch (rec->state) {
		case WREPL_STATE_ACTIVE:
			old_state	= "active";
			if (rec->is_static) {
				/*
				 *we store it again, so that it won't appear
				 * in the scavenging the next time
				 */
				old_state	= "active(static)";
				new_state	= "active(static)";
				modify_flags	= 0;
				modify_record	= true;
				break;
			}
			if (rec->type != WREPL_TYPE_SGROUP || !propagate) {
				new_state	= "released";
				rec->state	= WREPL_STATE_RELEASED;
				rec->expire_time= service->config.tombstone_interval + now;
				modify_flags	= 0;
				modify_record	= true;
				break;
			}
			/* check if there's any replica address */
			for (i=0;rec->addresses[i];i++) {
				if (strcmp(rec->addresses[i]->wins_owner, local_owner) != 0) {
					has_replicas = true;
					rec->addresses[i]->expire_time= service->config.renew_interval + now;
				}
			}
			if (has_replicas) {
				/* if it has replica addresses propagate them */
				new_state	= "active(propagated)";
				rec->state	= WREPL_STATE_ACTIVE;
				rec->expire_time= service->config.renew_interval + now;
				modify_flags	= WINSDB_FLAG_ALLOC_VERSION | WINSDB_FLAG_TAKE_OWNERSHIP;
				modify_record	= true;
				break;
			}
			/*
			 * if it doesn't have replica addresses, make it a tombstone,
			 * so that the released owned addresses are propagated
			 */
			new_state	= "tombstone";
			rec->state	= WREPL_STATE_TOMBSTONE;
			rec->expire_time= time(NULL) +
					  service->config.tombstone_interval +
					  service->config.tombstone_timeout;
			modify_flags	= WINSDB_FLAG_ALLOC_VERSION | WINSDB_FLAG_TAKE_OWNERSHIP;
			modify_record	= true;
			break;

		case WREPL_STATE_RELEASED:
			old_state	= "released";
			new_state	= "tombstone";
			rec->state	= WREPL_STATE_TOMBSTONE;
			rec->expire_time= service->config.tombstone_timeout + now;
			modify_flags	= WINSDB_FLAG_ALLOC_VERSION | WINSDB_FLAG_TAKE_OWNERSHIP;
			modify_record	= true;
			break;

		case WREPL_STATE_TOMBSTONE:
			old_state	= "tombstone";
			new_state	= "tombstone";
			if (!delete_tombstones) break;
			new_state	= "deleted";
			delete_record = true;
			break;

		case WREPL_STATE_RESERVED:
			DEBUG(0,("%s: corrupted record: %s\n",
				__location__, nbt_name_string(rec, rec->name)));
			return NT_STATUS_INTERNAL_DB_CORRUPTION;
		}

		if (modify_record) {
			action = "modify";
			ret = winsdb_modify(service->wins_db, rec, modify_flags);
		} else if (delete_record) {
			action = "delete";
			ret = winsdb_delete(service->wins_db, rec);
		} else {
			action = "skip";
			ret = NBT_RCODE_OK;
		}

		if (ret != NBT_RCODE_OK) {
			DEBUG(2,("WINS scavenging: failed to %s name %s (owned:%s -> owned:%s): error:%u\n",
				action, nbt_name_string(rec, rec->name), old_state, new_state, ret));
		} else {
			DEBUG(4,("WINS scavenging: %s name: %s (owned:%s -> owned:%s)\n",
				action, nbt_name_string(rec, rec->name), old_state, new_state));
		}

		talloc_free(rec);
	}

	return NT_STATUS_OK;
}

static NTSTATUS wreplsrv_scavenging_replica_non_active_records(struct wreplsrv_service *service, TALLOC_CTX *tmp_mem)
{
	NTSTATUS status;
	struct winsdb_record *rec = NULL;
	struct ldb_result *res = NULL;
	const char *owner_filter;
	const char *filter;
	unsigned int i;
	int ret;
	time_t now = time(NULL);
	const char *now_timestr;
	const char *action;
	const char *old_state=NULL;
	const char *new_state=NULL;
	uint32_t modify_flags;
	bool modify_record;
	bool delete_record;
	bool delete_tombstones;
	struct timeval tombstone_extra_time;

	now_timestr = ldb_timestring(tmp_mem, now);
	NT_STATUS_HAVE_NO_MEMORY(now_timestr);
	owner_filter = wreplsrv_owner_filter(service, tmp_mem,
					     service->wins_db->local_owner);
	NT_STATUS_HAVE_NO_MEMORY(owner_filter);
	filter = talloc_asprintf(tmp_mem,
				 "(&(!%s)(objectClass=winsRecord)"
				 "(!(recordState=%u))(expireTime<=%s))",
				 owner_filter, WREPL_STATE_ACTIVE, now_timestr);
	NT_STATUS_HAVE_NO_MEMORY(filter);
	ret = ldb_search(service->wins_db->ldb, tmp_mem, &res, NULL, LDB_SCOPE_SUBTREE, NULL, "%s", filter);
	if (ret != LDB_SUCCESS) return NT_STATUS_INTERNAL_DB_CORRUPTION;
	DEBUG(10,("WINS scavenging: filter '%s' count %d\n", filter, res->count));

	tombstone_extra_time = timeval_add(&service->startup_time,
					   service->config.tombstone_extra_timeout,
					   0);
	delete_tombstones = timeval_expired(&tombstone_extra_time);

	for (i=0; i < res->count; i++) {
		/*
		 * we pass '0' as 'now' here,
		 * because we want to get the raw timestamps which are in the DB
		 */
		status = winsdb_record(service->wins_db, res->msgs[i], tmp_mem, 0, &rec);
		NT_STATUS_NOT_OK_RETURN(status);
		talloc_free(res->msgs[i]);

		modify_flags	= 0;
		modify_record	= false;
		delete_record	= false;

		switch (rec->state) {
		case WREPL_STATE_ACTIVE:
			DEBUG(0,("%s: corrupted record: %s\n",
				__location__, nbt_name_string(rec, rec->name)));
			return NT_STATUS_INTERNAL_DB_CORRUPTION;

		case WREPL_STATE_RELEASED:
			old_state	= "released";
			new_state	= "tombstone";
			rec->state	= WREPL_STATE_TOMBSTONE;
			rec->expire_time= service->config.tombstone_timeout + now;
			modify_flags	= 0;
			modify_record	= true;
			break;

		case WREPL_STATE_TOMBSTONE:
			old_state	= "tombstone";
			new_state	= "tombstone";
			if (!delete_tombstones) break;
			new_state	= "deleted";
			delete_record = true;
			break;

		case WREPL_STATE_RESERVED:
			DEBUG(0,("%s: corrupted record: %s\n",
				__location__, nbt_name_string(rec, rec->name)));
			return NT_STATUS_INTERNAL_DB_CORRUPTION;
		}

		if (modify_record) {
			action = "modify";
			ret = winsdb_modify(service->wins_db, rec, modify_flags);
		} else if (delete_record) {
			action = "delete";
			ret = winsdb_delete(service->wins_db, rec);
		} else {
			action = "skip";
			ret = NBT_RCODE_OK;
		}

		if (ret != NBT_RCODE_OK) {
			DEBUG(2,("WINS scavenging: failed to %s name %s (replica:%s -> replica:%s): error:%u\n",
				action, nbt_name_string(rec, rec->name), old_state, new_state, ret));
		} else {
			DEBUG(4,("WINS scavenging: %s name: %s (replica:%s -> replica:%s)\n",
				action, nbt_name_string(rec, rec->name), old_state, new_state));
		}

		talloc_free(rec);
	}

	return NT_STATUS_OK;
}

struct verify_state {
	struct imessaging_context *msg_ctx;
	struct wreplsrv_service *service;
	struct winsdb_record *rec;
	struct nbtd_proxy_wins_challenge r;
};

static void verify_handler(struct tevent_req *subreq)
{
	struct verify_state *s =
		tevent_req_callback_data(subreq,
		struct verify_state);
	struct winsdb_record *rec = s->rec;
	const char *action;
	const char *old_state = "active";
	const char *new_state = "active";
	const char *new_owner = "replica";
	uint32_t modify_flags = 0;
	bool modify_record = false;
	bool delete_record = false;
	bool different = false;
	int ret;
	NTSTATUS status;
	uint32_t i, j;

	/*
	 * - if the name isn't present anymore remove our record
	 * - if the name is found and not a normal group check if the addresses match,
	 *   - if they don't match remove the record
	 *   - if they match do nothing
	 * - if an error happens do nothing
	 */
	status = dcerpc_nbtd_proxy_wins_challenge_r_recv(subreq, s);
	TALLOC_FREE(subreq);
	if (NT_STATUS_EQUAL(NT_STATUS_OBJECT_NAME_NOT_FOUND, status)) {
		delete_record = true;
		new_state = "deleted";
	} else if (NT_STATUS_IS_OK(status) && rec->type != WREPL_TYPE_GROUP) {
		for (i=0; i < s->r.out.num_addrs; i++) {
			bool found = false;
			for (j=0; rec->addresses[j]; j++) {
				if (strcmp(s->r.out.addrs[i].addr, rec->addresses[j]->address) == 0) {
					found = true;
					break;
				}
			}
			if (!found) {
				different = true;
				break;
			}
		}
	} else if (NT_STATUS_IS_OK(status) && rec->type == WREPL_TYPE_GROUP) {
		if (s->r.out.num_addrs != 1 || strcmp(s->r.out.addrs[0].addr, "255.255.255.255") != 0) {
			different = true;
		}
	}

	if (different) {
		/*
		 * if the reply from the owning wins server has different addresses
		 * then take the ownership of the record and make it a tombstone
		 * this will then hopefully replicated to the original owner of the record
		 * which will then propagate it's own record, so that the current record will
		 * be replicated to to us
		 */
		DEBUG(2,("WINS scavenging: replica %s verify got different addresses from winsserver: %s: tombstoning record\n",
			nbt_name_string(rec, rec->name), rec->wins_owner));

		rec->state	= WREPL_STATE_TOMBSTONE;
		rec->expire_time= time(NULL) + s->service->config.tombstone_timeout;
		for (i=0; rec->addresses[i]; i++) {
			rec->addresses[i]->expire_time = rec->expire_time;
		}
		modify_record	= true;
		modify_flags	= WINSDB_FLAG_ALLOC_VERSION | WINSDB_FLAG_TAKE_OWNERSHIP;
		new_state	= "tombstone";
		new_owner	= "owned";
	} else if (NT_STATUS_IS_OK(status)) {
		/* if the addresses are the same, just update the timestamps */
		rec->expire_time = time(NULL) + s->service->config.verify_interval;
		for (i=0; rec->addresses[i]; i++) {
			rec->addresses[i]->expire_time = rec->expire_time;
		}
		modify_record	= true;
		modify_flags	= 0;
		new_state	= "active";
	}

	if (modify_record) {
		action = "modify";
		ret = winsdb_modify(s->service->wins_db, rec, modify_flags);
	} else if (delete_record) {
		action = "delete";
		ret = winsdb_delete(s->service->wins_db, rec);
	} else {
		action = "skip";
		ret = NBT_RCODE_OK;
	}

	if (ret != NBT_RCODE_OK) {
		DEBUG(2,("WINS scavenging: failed to %s name %s (replica:%s -> %s:%s): error:%u\n",
			action, nbt_name_string(rec, rec->name), old_state, new_owner, new_state, ret));
	} else {
		DEBUG(4,("WINS scavenging: %s name: %s (replica:%s -> %s:%s): %s: %s\n",
			action, nbt_name_string(rec, rec->name), old_state, new_owner, new_state,
			rec->wins_owner, nt_errstr(status)));
	}

	talloc_free(s);
}

static NTSTATUS wreplsrv_scavenging_replica_active_records(struct wreplsrv_service *service, TALLOC_CTX *tmp_mem)
{
	NTSTATUS status;
	struct winsdb_record *rec = NULL;
	struct ldb_result *res = NULL;
	const char *owner_filter;
	const char *filter;
	unsigned int i;
	int ret;
	time_t now = time(NULL);
	const char *now_timestr;
	struct tevent_req *subreq;
	struct verify_state *s;
	struct dcerpc_binding_handle *irpc_handle;

	now_timestr = ldb_timestring(tmp_mem, now);
	NT_STATUS_HAVE_NO_MEMORY(now_timestr);
	owner_filter = wreplsrv_owner_filter(service, tmp_mem,
					     service->wins_db->local_owner);
	NT_STATUS_HAVE_NO_MEMORY(owner_filter);
	filter = talloc_asprintf(tmp_mem,
				 "(&(!%s)(objectClass=winsRecord)"
				 "(recordState=%u)(expireTime<=%s))",
				 owner_filter, WREPL_STATE_ACTIVE, now_timestr);
	NT_STATUS_HAVE_NO_MEMORY(filter);
	ret = ldb_search(service->wins_db->ldb, tmp_mem, &res, NULL, LDB_SCOPE_SUBTREE, NULL, "%s", filter);
	if (ret != LDB_SUCCESS) return NT_STATUS_INTERNAL_DB_CORRUPTION;
	DEBUG(10,("WINS scavenging: filter '%s' count %d\n", filter, res->count));

	for (i=0; i < res->count; i++) {
		/*
		 * we pass '0' as 'now' here,
		 * because we want to get the raw timestamps which are in the DB
		 */
		status = winsdb_record(service->wins_db, res->msgs[i], tmp_mem, 0, &rec);
		NT_STATUS_NOT_OK_RETURN(status);
		talloc_free(res->msgs[i]);

		if (rec->state != WREPL_STATE_ACTIVE) {
			DEBUG(0,("%s: corrupted record: %s\n",
				__location__, nbt_name_string(rec, rec->name)));
			return NT_STATUS_INTERNAL_DB_CORRUPTION;
		}

		/* 
		 * ask the owning wins server if the record still exists,
		 * if not delete the record
		 *
		 * TODO: NOTE: this is a simpliefied version, to verify that
		 *             a record still exist, I assume that w2k3 uses
		 *             DCERPC calls or some WINSREPL packets for this,
		 *             but we use a wins name query
		 */
		DEBUG(2,("ask wins server '%s' if '%s' with version_id:%llu still exists\n",
			 rec->wins_owner, nbt_name_string(rec, rec->name), 
			 (unsigned long long)rec->version));

		s = talloc_zero(tmp_mem, struct verify_state);
		NT_STATUS_HAVE_NO_MEMORY(s);
		s->msg_ctx	= service->task->msg_ctx;
		s->service	= service;
		s->rec		= talloc_steal(s, rec);

		s->r.in.name		= *rec->name;
		s->r.in.num_addrs	= 1;
		s->r.in.addrs		= talloc_array(s, struct nbtd_proxy_wins_addr, s->r.in.num_addrs);
		NT_STATUS_HAVE_NO_MEMORY(s->r.in.addrs);
		/* TODO: fix pidl to handle inline ipv4address arrays */
		s->r.in.addrs[0].addr	= rec->wins_owner;

		irpc_handle = irpc_binding_handle_by_name(s,
							  service->task->msg_ctx,
							  "nbt_server",
							  &ndr_table_irpc);
		if (irpc_handle == NULL) {
			return NT_STATUS_INTERNAL_ERROR;
		}

		subreq = dcerpc_nbtd_proxy_wins_challenge_r_send(s,
								 service->task->event_ctx,
								 irpc_handle,
								 &s->r);
		NT_STATUS_HAVE_NO_MEMORY(subreq);

		tevent_req_set_callback(subreq, verify_handler, s);

		talloc_steal(service, s);
	}

	return NT_STATUS_OK;
}

NTSTATUS wreplsrv_scavenging_run(struct wreplsrv_service *service)
{
	NTSTATUS status;
	TALLOC_CTX *tmp_mem;
	bool skip_first_run = false;

	if (!timeval_expired(&service->scavenging.next_run)) {
		return NT_STATUS_OK;
	}

	if (timeval_is_zero(&service->scavenging.next_run)) {
		skip_first_run = true;
	}

	service->scavenging.next_run = timeval_current_ofs(service->config.scavenging_interval, 0);
	status = wreplsrv_periodic_schedule(service, service->config.scavenging_interval);
	NT_STATUS_NOT_OK_RETURN(status);

	/*
	 * if it's the first time this functions is called (startup)
	 * the next_run is zero, in this case we should not do scavenging
	 */
	if (skip_first_run) {
		return NT_STATUS_OK;
	}

	if (service->scavenging.processing) {
		return NT_STATUS_OK;
	}

	DEBUG(2,("wreplsrv_scavenging_run(): start\n"));

	tmp_mem = talloc_new(service);
	NT_STATUS_HAVE_NO_MEMORY(tmp_mem);
	service->scavenging.processing = true;
	status = wreplsrv_scavenging_owned_records(service,tmp_mem);
	service->scavenging.processing = false;
	talloc_free(tmp_mem);
	NT_STATUS_NOT_OK_RETURN(status);

	tmp_mem = talloc_new(service);	
	NT_STATUS_HAVE_NO_MEMORY(tmp_mem);
	service->scavenging.processing = true;
	status = wreplsrv_scavenging_replica_non_active_records(service, tmp_mem);
	service->scavenging.processing = false;
	talloc_free(tmp_mem);
	NT_STATUS_NOT_OK_RETURN(status);

	tmp_mem = talloc_new(service);
	NT_STATUS_HAVE_NO_MEMORY(tmp_mem);
	service->scavenging.processing = true;
	status = wreplsrv_scavenging_replica_active_records(service, tmp_mem);
	service->scavenging.processing = false;
	talloc_free(tmp_mem);
	NT_STATUS_NOT_OK_RETURN(status);

	DEBUG(2,("wreplsrv_scavenging_run(): end\n"));

	return NT_STATUS_OK;
}
