/* 
 * FreeSWITCH Modular Media Switching Software Library / Soft-Switch Application
 * Copyright (C) 2005/2006, Anthony Minessale II <anthmct@yahoo.com>
 *
 * Version: MPL 1.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is FreeSWITCH Modular Media Switching Software Library / Soft-Switch Application
 *
 * The Initial Developer of the Original Code is
 * Anthony Minessale II <anthmct@yahoo.com>
 * Portions created by the Initial Developer are Copyright (C)
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 * 
 * Anthony Minessale II <anthmct@yahoo.com>
 *
 * mod_enum.c -- ENUM
 *
 */

#include <switch.h>
#include <udns.h>

#ifndef WIN32
#define closesocket close
#endif

static const char modname[] = "mod_enum";

struct enum_record {
	int order;
	int preference;
	char *service;
	char *route;
	struct enum_record *next;
};
typedef struct enum_record enum_record_t;

struct query {
	const char *name;			/* original query string */
	char *number;
	unsigned char dn[DNS_MAXDN];
	enum dns_type qtyp;			/* type of the query */
	enum_record_t *results;
};
typedef struct query enum_query_t;

struct route {
	char *service;
	char *regex;
	char *replace;
	struct route *next;
};
typedef struct route enum_route_t;

static enum dns_class qcls = DNS_C_IN;


static struct {
	char *root;
	switch_hash_t *routes;
	enum_route_t *route_order;
	switch_memory_pool_t *pool;
} globals;

SWITCH_DECLARE_GLOBAL_STRING_FUNC(set_global_root, globals.root)


	 static void add_route(char *service, char *regex, char *replace)
{
	enum_route_t *route, *rp;

	if (!(route = malloc(sizeof(*route)))) {
		return;
	}

	memset(route, 0, sizeof(*route));

	route->service = strdup(service);
	route->regex = strdup(regex);
	route->replace = strdup(replace);

	if (!globals.route_order) {
		globals.route_order = route;
	} else {
		for (rp = globals.route_order; rp && rp->next; rp = rp->next);
		rp->next = route;
	}

	if (!(rp = switch_core_hash_find(globals.routes, service))) {
		switch_core_hash_insert(globals.routes, route->service, route);
	}
}


static switch_status_t load_config(void)
{
	char *cf = "enum.conf";
	switch_xml_t cfg, xml = NULL, param, settings, route, routes;
	switch_status_t status = SWITCH_STATUS_SUCCESS;

	memset(&globals, 0, sizeof(globals));
	if (switch_core_new_memory_pool(&globals.pool) != SWITCH_STATUS_SUCCESS) {
		switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_CRIT, "OH OH no pool\n");
		status = SWITCH_STATUS_TERM;
		goto done;
	}

	switch_core_hash_init(&globals.routes, globals.pool);

	if (!(xml = switch_xml_open_cfg(cf, &cfg, NULL))) {
		switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "open of %s failed\n", cf);
		status = SWITCH_STATUS_FALSE;
		goto done;
	}

	if ((settings = switch_xml_child(cfg, "settings"))) {
		for (param = switch_xml_child(settings, "param"); param; param = param->next) {
			char *var = (char *) switch_xml_attr_soft(param, "name");
			char *val = (char *) switch_xml_attr_soft(param, "value");
			if (!strcasecmp(var, "default-root")) {
				set_global_root(val);
			} else if (!strcasecmp(var, "log-level-trace")) {

			}
		}
	}

	if ((routes = switch_xml_child(cfg, "routes"))) {
		for (route = switch_xml_child(routes, "route"); route; route = route->next) {
			char *service = (char *) switch_xml_attr_soft(route, "service");
			char *regex = (char *) switch_xml_attr_soft(route, "regex");
			char *replace = (char *) switch_xml_attr_soft(route, "replace");

			if (service && regex && replace) {
				add_route(service, regex, replace);
			} else {
				switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Invalid Route!\n");
			}
		}
	}

  done:

	if (xml) {
		switch_xml_free(xml);
	}

	if (!globals.root) {
		set_global_root("e164.org");
	}

	return status;

}


static char *reverse_number(char *in, char *root)
{
	switch_size_t len;
	char *out = NULL;
	char *y, *z;

	if (!(in && root)) {
		return NULL;
	}

	len = (strlen(in) * 2) + strlen(root) + 1;
	if ((out = malloc(len))) {
		memset(out, 0, len);

		z = out;
		for (y = in + (strlen(in) - 1); y; y--) {
			if (*y > 47 && *y < 58) {
				*z++ = *y;
				*z++ = '.';
			}
			if (y == in) {
				break;
			}
		}
		strcat(z, root);
	}

	return out;
}

static void dnserror(enum_query_t * q, int errnum)
{

	switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "unable to lookup %s record for %s: %s\n",
					  dns_typename(q->qtyp), dns_dntosp(q->dn), dns_strerror(errnum));
}


static void add_result(enum_query_t * q, int order, int preference, char *service, char *route)
{
	enum_record_t *new_result, *rp, *prev = NULL;


	if (!(new_result = malloc(sizeof(*new_result)))) {
		return;
	}

	memset(new_result, 0, sizeof(*new_result));

	new_result->order = order;
	new_result->preference = preference;
	new_result->service = strdup(service);
	new_result->route = strdup(route);


	if (!q->results) {
		q->results = new_result;
		return;
	}

	rp = q->results;

	while (rp && strcmp(rp->service, new_result->service)) {
		prev = rp;
		rp = rp->next;
	}

	while (rp && !strcmp(rp->service, new_result->service) && new_result->order > rp->order) {
		prev = rp;
		rp = rp->next;
	}

	while (rp && !strcmp(rp->service, new_result->service) && new_result->preference > rp->preference) {
		prev = rp;
		rp = rp->next;
	}

	if (prev) {
		new_result->next = rp;
		prev->next = new_result;
	} else {
		new_result->next = rp;
		q->results = new_result;
	}
}


static void free_results(enum_record_t ** results)
{
	enum_record_t *fp, *rp;

	for (rp = *results; rp;) {
		fp = rp;
		rp = rp->next;
		switch_safe_free(fp->service);
		switch_safe_free(fp->route);
		switch_safe_free(fp);
	}
	*results = NULL;
}

static void parse_rr(const struct dns_parse *p, enum_query_t * q, struct dns_rr *rr)
{
	const unsigned char *pkt = p->dnsp_pkt;
	const unsigned char *end = p->dnsp_end;
	const unsigned char *dptr = rr->dnsrr_dptr;
	const unsigned char *dend = rr->dnsrr_dend;
	unsigned char *dn = rr->dnsrr_dn;
	const unsigned char *c;
	char *ptr;
	char flags;
	int order;
	int preference;
	char *nme;
	char *service = NULL;
	char *regex = NULL;
	char *replace = NULL;
	int argc = 0;
	char *argv[4] = { 0 };
	int leap;

	switch (rr->dnsrr_typ) {

	case DNS_T_NAPTR:			/* prio weight port targetDN */
		c = dptr;
		c += 2 + 2 + 2;
		if (dns_getdn(pkt, &c, end, dn, DNS_MAXDN) <= 0 || c != dend)
			goto xperr;
		c = dptr;

		leap = *dn;
		nme = (char *) dn + 1;

		order = dns_get16(c + 0);
		preference = dns_get16(c + 2);
		flags = (char) dns_get16(c + 4);

		if ((ptr = nme + leap)) {
			service = nme;
			*ptr++ = '\0';
			argc = switch_separate_string(ptr, '!', argv, (sizeof(argv) / sizeof(argv[0])));
			regex = argv[1];
			replace = argv[2];
		}

		for (ptr = replace; ptr && *ptr; ptr++) {
			if (*ptr == '\\') {
				*ptr = '$';
			}
		}

		if (flags && service && regex && replace) {
			switch_regex_t *re = NULL;
			int proceed = 0, ovector[30];
			char substituted[1024] = "";
			char rbuf[1024] = "";
			char *uri;
			enum_route_t *route;

			switch_regex_safe_free(re);

			if ((proceed = switch_regex_perform(q->number, regex, &re, ovector, sizeof(ovector) / sizeof(ovector[0])))) {
				if (strchr(regex, '(')) {
					switch_perform_substitution(re, proceed, replace, q->number, substituted, sizeof(substituted), ovector);
					uri = substituted;
				} else {
					uri = replace;
				}

				if ((route = (enum_route_t *) switch_core_hash_find(globals.routes, service))) {
					switch_regex_safe_free(re);
					if ((proceed = switch_regex_perform(uri, route->regex, &re, ovector, sizeof(ovector) / sizeof(ovector[0])))) {
						if (strchr(route->regex, '(')) {
							switch_perform_substitution(re, proceed, route->replace, uri, rbuf, sizeof(rbuf), ovector);
							uri = rbuf;
						} else {
							uri = route->replace;
						}
					}
				}

				add_result(q, order, preference, service, uri);
			}

			switch_regex_safe_free(re);
		}

		break;

	default:
		break;
	}

	return;

  xperr:
	//printf("<parse error>\n");
	return;
}

static void dnscb(struct dns_ctx *ctx, void *result, void *data)
{
	int r = dns_status(ctx);
	enum_query_t *q = data;
	struct dns_parse p;
	struct dns_rr rr;
	unsigned nrr;
	unsigned char dn[DNS_MAXDN];
	const unsigned char *pkt, *cur, *end, *qdn;
	if (!result) {
		dnserror(q, r);
		return;
	}
	pkt = result;
	end = pkt + r;
	cur = dns_payload(pkt);
	dns_getdn(pkt, &cur, end, dn, sizeof(dn));
	dns_initparse(&p, NULL, pkt, cur, end);
	p.dnsp_qcls = p.dnsp_qtyp = 0;
	qdn = dn;
	nrr = 0;
	while ((r = dns_nextrr(&p, &rr)) > 0) {
		if (!dns_dnequal(qdn, rr.dnsrr_dn))
			continue;
		if ((qcls == DNS_C_ANY || qcls == rr.dnsrr_cls) && (q->qtyp == DNS_T_ANY || q->qtyp == rr.dnsrr_typ))
			++nrr;
		else if (rr.dnsrr_typ == DNS_T_CNAME && !nrr) {
			if (dns_getdn(pkt, &rr.dnsrr_dptr, end, p.dnsp_dnbuf, sizeof(p.dnsp_dnbuf)) <= 0 || rr.dnsrr_dptr != rr.dnsrr_dend) {
				r = DNS_E_PROTOCOL;
				break;
			} else {
				qdn = p.dnsp_dnbuf;
			}
		}
	}
	if (!r && !nrr)
		r = DNS_E_NODATA;
	if (r < 0) {
		dnserror(q, r);
		free(result);
		return;
	}

	dns_rewind(&p, NULL);
	p.dnsp_qtyp = q->qtyp;
	p.dnsp_qcls = qcls;
	while (dns_nextrr(&p, &rr)) {
		parse_rr(&p, q, &rr);
	}

	free(result);
}


static switch_status_t enum_lookup(char *root, char *in, enum_record_t ** results)
{
	switch_status_t sstatus = SWITCH_STATUS_SUCCESS;
	char *name = NULL;
	enum_query_t query = { 0 };
	enum dns_type l_qtyp = DNS_T_NAPTR;
	int i = 0, abs = 0;
	dns_socket fd = (dns_socket) - 1;
	fd_set fds;
	struct timeval tv = { 0 };
	time_t now = 0;
	struct dns_ctx *nctx = NULL;
	char *num, *mnum = NULL;

	if (*in != '+') {
		mnum = switch_mprintf("+%s", in);
		num = mnum;
	} else {
		num = in;
	}

	if (!(name = reverse_number(num, root))) {
		switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "Parse Error!\n");
		sstatus = SWITCH_STATUS_FALSE;
		goto done;
	}

	if (!(nctx = dns_new(NULL))) {
		switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "Memory Error!\n");
		sstatus = SWITCH_STATUS_MEMERR;
		goto done;
	}

	fd = dns_open(nctx);

	if (fd < 0) {
		switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "FD Error!\n");
		sstatus = SWITCH_STATUS_FALSE;
		goto done;
	}

	dns_ptodn(name, (unsigned int) strlen(name), query.dn, sizeof(query.dn), &abs);
	query.name = name;
	query.number = num;
	query.qtyp = l_qtyp;

	if (abs) {
		abs = DNS_NOSRCH;
	}

	if (!dns_submit_dn(nctx, query.dn, qcls, l_qtyp, abs, 0, dnscb, &query)) {
		dnserror(&query, dns_status(nctx));
	}

	FD_ZERO(&fds);
	now = 0;

	while ((i = dns_timeouts(nctx, -1, now)) > 0) {
#ifdef _MSC_VER
#pragma warning(push)
#pragma warning(disable: 4389 4127)
#endif
		FD_SET(fd, &fds);
#ifdef _MSC_VER
#pragma warning(pop)
#endif
		tv.tv_sec = i;
		tv.tv_usec = 0;
		i = select((int) (fd + 1), &fds, 0, 0, &tv);
		now = time(NULL);
		if (i > 0)
			dns_ioevent(nctx, now);
	}

	if (!query.results) {
		sstatus = SWITCH_STATUS_FALSE;
	}

	*results = query.results;
	query.results = NULL;

  done:

	if (fd > -1) {
		closesocket(fd);
		fd = (dns_socket) - 1;
	}

	if (nctx) {
		dns_free(nctx);
	}

	switch_safe_free(name);
	switch_safe_free(mnum);

	return sstatus;
}


static switch_caller_extension_t *enum_dialplan_hunt(switch_core_session_t *session, void *arg)
{
	switch_caller_extension_t *extension = NULL;
	switch_caller_profile_t *caller_profile;
	enum_record_t *results, *rp;
	switch_channel_t *channel = switch_core_session_get_channel(session);
	enum_route_t *rtp;
	char *dp = (char *) arg;

	assert(channel != NULL);

	caller_profile = switch_channel_get_caller_profile(channel);

	switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "ENUM Lookup on %s\n", caller_profile->destination_number);

	if (enum_lookup(switch_strlen_zero(dp) ? globals.root : dp, caller_profile->destination_number, &results) == SWITCH_STATUS_SUCCESS) {
		if ((extension = switch_caller_extension_new(session, caller_profile->destination_number, caller_profile->destination_number)) == 0) {
			switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_CRIT, "memory error!\n");
			free_results(&results);
			return NULL;
		}
		switch_channel_set_variable(channel, SWITCH_HANGUP_AFTER_BRIDGE_VARIABLE, "true");

		for (rtp = globals.route_order; rtp; rtp = rtp->next) {
			for (rp = results; rp; rp = rp->next) {
				if (!strcmp(rtp->service, rp->service)) {
					switch_caller_extension_add_application(session, extension, "bridge", rp->route);
				}
			}
		}

		free_results(&results);
	}

	if (extension) {
		switch_channel_set_state(channel, CS_EXECUTE);
	}

	return extension;

}

static void enum_app_function(switch_core_session_t *session, char *data)
{
	int argc = 0;
	char *argv[4] = { 0 };
	char *mydata = NULL;
	char *dest = NULL, *root = NULL;
	enum_record_t *results, *rp;
	enum_route_t *rtp;
	char rbuf[1024] = "";
	char vbuf[1024] = "";
	char *rbp = rbuf;
	switch_size_t l = 0, rbl = sizeof(rbuf);
	uint32_t cnt = 1;
	switch_channel_t *channel = switch_core_session_get_channel(session);

	assert(channel != NULL);

	if (!(mydata = switch_core_session_strdup(session, data))) {
		return;
	}

	if ((argc = switch_separate_string(mydata, ' ', argv, (sizeof(argv) / sizeof(argv[0]))))) {
		dest = argv[0];
		root = argv[1] ? argv[1] : globals.root;
		if (enum_lookup(root, data, &results) == SWITCH_STATUS_SUCCESS) {
			switch_hash_index_t *hi;
			void *vval;
			const void *vvar;

			for (hi = switch_channel_variable_first(channel, switch_core_session_get_pool(session)); hi; hi = switch_hash_next(hi)) {
				switch_hash_this(hi, &vvar, NULL, &vval);
				if (vvar && !strncmp(vvar, "enum_", 5)) {
					switch_channel_set_variable(channel, (char *) vvar, NULL);
				}
			}

			for (rtp = globals.route_order; rtp; rtp = rtp->next) {
				for (rp = results; rp; rp = rp->next) {
					if (!strcmp(rtp->service, rp->service)) {
						snprintf(vbuf, sizeof(vbuf), "enum_route_%d", cnt++);
						switch_channel_set_variable(channel, vbuf, rp->route);

						snprintf(rbp, rbl, "%s|", rp->route);
						l = strlen(rp->route) + 1;
						rbp += l;
						rbl -= l;
					}
				}
			}
			snprintf(vbuf, sizeof(vbuf), "%d", cnt);
			switch_channel_set_variable(channel, "enum_route_count", vbuf);
			*(rbuf + strlen(rbuf) - 1) = '\0';
			switch_channel_set_variable(channel, "enum_auto_route", rbuf);
			free_results(&results);
		}
	}

}

static switch_status_t enum_function(char *data, switch_core_session_t *session, switch_stream_handle_t *stream)
{

	int argc = 0;
	char *argv[4] = { 0 };
	enum_record_t *results, *rp;
	char *mydata = NULL;
	char *dest = NULL, *root = NULL;
	enum_route_t *rtp;

	if (session) {
		switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "This function cannot be called from the dialplan.\n");
		return SWITCH_STATUS_FALSE;
	}

	if (!(mydata = strdup(data))) {
		stream->write_function(stream, "Error!\n");
		return SWITCH_STATUS_FALSE;
	}

	if ((argc = switch_separate_string(mydata, ' ', argv, (sizeof(argv) / sizeof(argv[0]))))) {
		dest = argv[0];
		root = argv[1] ? argv[1] : globals.root;

		if (!enum_lookup(root, data, &results) == SWITCH_STATUS_SUCCESS) {
			stream->write_function(stream, "No Match!\n");
			return SWITCH_STATUS_SUCCESS;
		}

		stream->write_function(stream,
							   "\nOffered Routes:\n"
							   "Order\tPref\tService   \tRoute\n" "==============================================================================\n");

		for (rp = results; rp; rp = rp->next) {
			stream->write_function(stream, "%d\t%d\t%-10s\t%s\n", rp->order, rp->preference, rp->service, rp->route);
		}


		stream->write_function(stream,
							   "\nSupported Routes:\n"
							   "Order\tPref\tService   \tRoute\n" "==============================================================================\n");

		for (rtp = globals.route_order; rtp; rtp = rtp->next) {
			for (rp = results; rp; rp = rp->next) {
				if (!strcmp(rtp->service, rp->service)) {
					stream->write_function(stream, "%d\t%d\t%-10s\t%s\n", rp->order, rp->preference, rp->service, rp->route);
				}
			}
		}

		free_results(&results);
	} else {
		stream->write_function(stream, "Invalid Input!\n");
	}

	return SWITCH_STATUS_SUCCESS;
}


static const switch_dialplan_interface_t enum_dialplan_interface = {
	/*.interface_name = */ "enum",
	/*.hunt_function = */ enum_dialplan_hunt
		/*.next = NULL */
};

static const switch_application_interface_t enum_application_interface = {
	/*.interface_name */ "enum",
	/*.application_function */ enum_app_function,
	/* long_desc */ "Perform an ENUM lookup",
	/* short_desc */ "Perform an ENUM lookup",
	/* syntax */ "<number> [<root>]",
	/* flags */ SAF_SUPPORT_NOMEDIA,
	/*.next */ NULL
};

static switch_api_interface_t enum_api_interface = {
	/*.interface_name */ "enum",
	/*.desc */ "ENUM",
	/*.function */ enum_function,
	/*.syntax */ "",
	/*.next */ NULL
};

static switch_loadable_module_interface_t enum_module_interface = {
	/*.module_name */ modname,
	/*.endpoint_interface */ NULL,
	/*.timer_interface */ NULL,
	/*.dialplan_interface */ &enum_dialplan_interface,
	/*.codec_interface */ NULL,
	/*.application_interface */ &enum_application_interface,
	/*.api_interface */ &enum_api_interface,
	/*.file_interface */ NULL,
	/*.speech_interface */ NULL,
	/*.directory_interface */ NULL
};

SWITCH_MOD_DECLARE(switch_status_t) switch_module_load(const switch_loadable_module_interface_t **module_interface, char *filename)
{

	if (dns_init(0) < 0) {
		return SWITCH_STATUS_FALSE;
	}

	load_config();

	/* connect my internal structure to the blank pointer passed to me */
	*module_interface = &enum_module_interface;

	/* indicate that the module should continue to be loaded */
	return SWITCH_STATUS_SUCCESS;
}
