/**
 *
 * This file is part of Kamailio, a free SIP server.
 *
 * This file 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 file 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, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 *
 */


#include <assert.h>
#include <ctype.h>
#include <string.h>
#include <stdlib.h>
#include <time.h>

#include "../../core/dprint.h"
#include "../../core/hashes.h"
#include "../../core/pvar.h"
#include "../../core/locking.h"

#include "geoip2_pv.h"

typedef struct _sr_geoip2_record
{
	MMDB_lookup_result_s record;
	str time_zone;
	str zip;
	str city;
	str region_code;
	str region_name;
	str country;
	str cont_code;
	char latitude[16];
	char longitude[16];
	char metro[16];
	char nmask[8];
	char tomatch[256];
	int flags;
} sr_geoip2_record_t;

typedef struct _sr_geoip2_item
{
	str pvclass;
	unsigned int hashid;
	sr_geoip2_record_t r;
	struct _sr_geoip2_item *next;
} sr_geoip2_item_t;

typedef struct _geoip2_pv
{
	sr_geoip2_item_t *item;
	int type;
} geoip2_pv_t;

static MMDB_s *_handle_GeoIP = NULL;
static gen_lock_t *_sr_geoip2_lock = NULL;

static sr_geoip2_item_t *_sr_geoip2_list = NULL;

static int *_sr_geoip2_reloadG = NULL;
static int _sr_geoip2_reloadL = 0;

MMDB_s *get_geoip_handle(void)
{
	return _handle_GeoIP;
}

gen_lock_t *get_gen_lock(void)
{
	return _sr_geoip2_lock;
}

sr_geoip2_record_t *sr_geoip2_get_record(str *name)
{
	sr_geoip2_item_t *it = NULL;
	unsigned int hashid = 0;

	hashid = get_hash1_raw(name->s, name->len);

	it = _sr_geoip2_list;
	while(it != NULL) {
		if(it->hashid == hashid && it->pvclass.len == name->len
				&& strncmp(it->pvclass.s, name->s, name->len) == 0)
			return &it->r;
		it = it->next;
	}
	return NULL;
}

sr_geoip2_item_t *sr_geoip2_add_item(str *name)
{
	sr_geoip2_item_t *it = NULL;
	unsigned int hashid = 0;

	hashid = get_hash1_raw(name->s, name->len);

	it = _sr_geoip2_list;
	while(it != NULL) {
		if(it->hashid == hashid && it->pvclass.len == name->len
				&& strncmp(it->pvclass.s, name->s, name->len) == 0)
			return it;
		it = it->next;
	}
	/* add new */
	it = (sr_geoip2_item_t *)pkg_malloc(sizeof(sr_geoip2_item_t));
	if(it == NULL) {
		LM_ERR("no more pkg\n");
		return NULL;
	}
	memset(it, 0, sizeof(sr_geoip2_item_t));
	it->pvclass.s = (char *)pkg_malloc(name->len + 1);
	if(it->pvclass.s == NULL) {
		LM_ERR("no more pkg.\n");
		pkg_free(it);
		return NULL;
	}
	memcpy(it->pvclass.s, name->s, name->len);
	it->pvclass.s[name->len] = '\0';
	it->pvclass.len = name->len;
	it->hashid = hashid;
	it->next = _sr_geoip2_list;
	_sr_geoip2_list = it;
	return it;
}

int sr_geoip2_add_resid(str *rname)
{
	if(sr_geoip2_add_item(rname) == NULL) {
		return -1;
	}
	return 0;
}

int pv_parse_geoip2_name(pv_spec_p sp, str *in)
{
	geoip2_pv_t *gpv = NULL;
	char *p;
	str pvc;
	str pvs;
	if(sp == NULL || in == NULL || in->len <= 0)
		return -1;

	gpv = (geoip2_pv_t *)pkg_malloc(sizeof(geoip2_pv_t));
	if(gpv == NULL)
		return -1;

	memset(gpv, 0, sizeof(geoip2_pv_t));

	p = in->s;

	while(p < in->s + in->len
			&& (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r'))
		p++;
	if(p > in->s + in->len || *p == '\0')
		goto error;
	pvc.s = p;
	while(p < in->s + in->len) {
		if(*p == '=' || *p == ' ' || *p == '\t' || *p == '\n' || *p == '\r')
			break;
		p++;
	}
	if(p > in->s + in->len || *p == '\0')
		goto error;
	pvc.len = p - pvc.s;
	if(*p != '=') {
		while(p < in->s + in->len
				&& (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r'))
			p++;
		if(p > in->s + in->len || *p == '\0' || *p != '=')
			goto error;
	}
	p++;
	if(*p != '>')
		goto error;
	p++;

	pvs.len = in->len - (int)(p - in->s);
	pvs.s = p;
	LM_DBG("geoip2 [%.*s] - key [%.*s]\n", pvc.len, pvc.s, pvs.len, pvs.s);

	gpv->item = sr_geoip2_add_item(&pvc);
	if(gpv->item == NULL)
		goto error;

	switch(pvs.len) {
		case 2:
			if(strncmp(pvs.s, "cc", 2) == 0)
				gpv->type = 0;
			else if(strncmp(pvs.s, "tz", 2) == 0)
				gpv->type = 1;
			else
				goto error;
			break;
		case 3:
			if(strncmp(pvs.s, "zip", 3) == 0)
				gpv->type = 2;
			else if(strncmp(pvs.s, "lat", 3) == 0)
				gpv->type = 3;
			else if(strncmp(pvs.s, "lon", 3) == 0)
				gpv->type = 4;
			else
				goto error;
			break;
		case 4:
			if(strncmp(pvs.s, "city", 4) == 0)
				gpv->type = 8;
			else if(strncmp(pvs.s, "regc", 4) == 0)
				gpv->type = 10;
			else if(strncmp(pvs.s, "regn", 4) == 0)
				gpv->type = 11;
			else
				goto error;
			break;
		case 5:
			if(strncmp(pvs.s, "metro", 5) == 0)
				gpv->type = 12;
			else if(strncmp(pvs.s, "nmask", 5) == 0)
				gpv->type = 13;
			else if(strncmp(pvs.s, "contc", 5) == 0)
				gpv->type = 6;
			else
				goto error;
			break;
		default:
			goto error;
	}
	sp->pvp.pvn.u.dname = (void *)gpv;
	sp->pvp.pvn.type = PV_NAME_OTHER;

	return 0;

error:
	if(gpv != NULL)
		pkg_free(gpv);

	LM_ERR("error at PV geoip2 name: %.*s\n", in->len, in->s);
	return -1;
}

int pv_geoip2_get_strzval(
		struct sip_msg *msg, pv_param_t *param, pv_value_t *res, char *sval)
{
	str s;
	if(sval == NULL)
		return pv_get_null(msg, param, res);

	s.s = sval;
	s.len = strlen(s.s);
	return pv_get_strval(msg, param, res, &s);
}

static int get_mmdb_value2(MMDB_entry_s *entry, MMDB_entry_data_s *data,
		const char *first, const char *second)
{
	int status = 0;

	lock_get(_sr_geoip2_lock);
	status = MMDB_get_value(entry, data, first, second, NULL);
	lock_release(_sr_geoip2_lock);

	return status;
}

static int get_mmdb_value3(MMDB_entry_s *entry, MMDB_entry_data_s *data,
		const char *first, const char *second, const char *third)
{
	int status = 0;

	lock_get(_sr_geoip2_lock);
	status = MMDB_get_value(entry, data, first, second, third, NULL);
	lock_release(_sr_geoip2_lock);

	return status;
}

static int get_mmdb_value4(MMDB_entry_s *entry, MMDB_entry_data_s *data,
		const char *first, const char *second, const char *third,
		const char *fourth)
{
	int status = 0;

	lock_get(_sr_geoip2_lock);
	status = MMDB_get_value(entry, data, first, second, third, fourth, NULL);
	lock_release(_sr_geoip2_lock);

	return status;
}


int pv_get_geoip2(struct sip_msg *msg, pv_param_t *param, pv_value_t *res)
{
	geoip2_pv_t *gpv;
	MMDB_entry_data_s entry_data;

	if(msg == NULL || param == NULL)
		return -1;

	gpv = (geoip2_pv_t *)param->pvn.u.dname;
	if(gpv == NULL)
		return -1;
	if(gpv->item == NULL)
		return pv_get_null(msg, param, res);

	switch(gpv->type) {
		case 1: /* tz */
			if(gpv->item->r.time_zone.s == NULL) {
				if(gpv->item->r.flags & 1)
					return pv_get_null(msg, param, res);
				if(get_mmdb_value2(&gpv->item->r.record.entry, &entry_data,
						   "location", "time_zone")
						!= MMDB_SUCCESS)
					return pv_get_null(msg, param, res);
				if(entry_data.has_data
						&& entry_data.type == MMDB_DATA_TYPE_UTF8_STRING) {
					gpv->item->r.time_zone.s = (char *)entry_data.utf8_string;
					gpv->item->r.time_zone.len = entry_data.data_size;
				}
				gpv->item->r.flags |= 1;
			}
			return pv_get_strval(msg, param, res, &gpv->item->r.time_zone);
		case 2: /* zip */
			if(gpv->item->r.zip.s == NULL) {
				if(gpv->item->r.flags & 32)
					return pv_get_null(msg, param, res);
				if(get_mmdb_value2(&gpv->item->r.record.entry, &entry_data,
						   "postal", "code")
						!= MMDB_SUCCESS)
					return pv_get_null(msg, param, res);
				if(entry_data.has_data
						&& entry_data.type == MMDB_DATA_TYPE_UTF8_STRING) {
					gpv->item->r.zip.s = (char *)entry_data.utf8_string;
					gpv->item->r.zip.len = entry_data.data_size;
				}
				gpv->item->r.flags |= 32;
			}
			return pv_get_strval(msg, param, res, &gpv->item->r.zip);
		case 3: /* lat */
			if((gpv->item->r.flags & 2) == 0) {
				gpv->item->r.latitude[0] = '\0';
				if(get_mmdb_value2(&gpv->item->r.record.entry, &entry_data,
						   "location", "latitude")
						!= MMDB_SUCCESS)
					return pv_get_null(msg, param, res);
				if(entry_data.has_data
						&& entry_data.type == MMDB_DATA_TYPE_DOUBLE)
					snprintf(gpv->item->r.latitude, 15, "%f",
							entry_data.double_value);
				gpv->item->r.flags |= 2;
			}
			return pv_geoip2_get_strzval(
					msg, param, res, gpv->item->r.latitude);
		case 4: /* lon */
			if((gpv->item->r.flags & 4) == 0) {
				gpv->item->r.latitude[0] = '\0';
				if(get_mmdb_value2(&gpv->item->r.record.entry, &entry_data,
						   "location", "longitude")
						!= MMDB_SUCCESS)
					return pv_get_null(msg, param, res);
				if(entry_data.has_data
						&& entry_data.type == MMDB_DATA_TYPE_DOUBLE)
					snprintf(gpv->item->r.longitude, 15, "%f",
							entry_data.double_value);
				gpv->item->r.flags |= 4;
			}
			return pv_geoip2_get_strzval(
					msg, param, res, gpv->item->r.longitude);
		case 6: /* contc */
			if(gpv->item->r.cont_code.s == NULL) {
				if(gpv->item->r.flags & 16)
					return pv_get_null(msg, param, res);
				if(get_mmdb_value2(&gpv->item->r.record.entry, &entry_data,
						   "continent", "code")
						!= MMDB_SUCCESS)
					return pv_get_null(msg, param, res);
				if(entry_data.has_data
						&& entry_data.type == MMDB_DATA_TYPE_UTF8_STRING) {
					gpv->item->r.cont_code.s = (char *)entry_data.utf8_string;
					gpv->item->r.cont_code.len = entry_data.data_size;
				}
				gpv->item->r.flags |= 16;
			}
			return pv_get_strval(msg, param, res, &gpv->item->r.cont_code);
		case 8: /* city */
			if(gpv->item->r.city.s == NULL) {
				if(gpv->item->r.flags & 64)
					return pv_get_null(msg, param, res);
				if(get_mmdb_value3(&gpv->item->r.record.entry, &entry_data,
						   "city", "names", "en")
						!= MMDB_SUCCESS)
					return pv_get_null(msg, param, res);
				if(entry_data.has_data
						&& entry_data.type == MMDB_DATA_TYPE_UTF8_STRING) {
					gpv->item->r.city.s = (char *)entry_data.utf8_string;
					gpv->item->r.city.len = entry_data.data_size;
				}
				gpv->item->r.flags |= 64;
			}
			return pv_get_strval(msg, param, res, &gpv->item->r.city);
		case 10: /* regc */
			if(gpv->item->r.region_code.s == NULL) {
				if(gpv->item->r.flags & 128)
					return pv_get_null(msg, param, res);
				if(get_mmdb_value3(&gpv->item->r.record.entry, &entry_data,
						   "subdivisions", "0", "iso_code")
						!= MMDB_SUCCESS)
					return pv_get_null(msg, param, res);
				if(entry_data.has_data
						&& entry_data.type == MMDB_DATA_TYPE_UTF8_STRING) {
					gpv->item->r.region_code.s = (char *)entry_data.utf8_string;
					gpv->item->r.region_code.len = entry_data.data_size;
				}
				gpv->item->r.flags |= 128;
			}
			return pv_get_strval(msg, param, res, &gpv->item->r.region_code);
		case 11: /* regn */
			if(gpv->item->r.region_name.s == NULL) {
				if(gpv->item->r.flags & 16)
					return pv_get_null(msg, param, res);
				if(get_mmdb_value4(&gpv->item->r.record.entry, &entry_data,
						   "subdivisions", "0", "names", "en")
						!= MMDB_SUCCESS)
					return pv_get_null(msg, param, res);
				if(entry_data.has_data
						&& entry_data.type == MMDB_DATA_TYPE_UTF8_STRING) {
					gpv->item->r.region_name.s = (char *)entry_data.utf8_string;
					gpv->item->r.region_name.len = entry_data.data_size;
				}
				gpv->item->r.flags |= 16;
			}
			return pv_get_strval(msg, param, res, &gpv->item->r.region_name);
		case 12: /* metro */
			if((gpv->item->r.flags & 256) == 0) {
				gpv->item->r.metro[0] = '\0';
				if(get_mmdb_value2(&gpv->item->r.record.entry, &entry_data,
						   "location", "metro_code")
						!= MMDB_SUCCESS)
					return pv_get_null(msg, param, res);
				if(entry_data.has_data
						&& entry_data.type == MMDB_DATA_TYPE_UINT16)
					snprintf(gpv->item->r.metro, 15, "%hd", entry_data.uint16);
				gpv->item->r.flags |= 256;
			}
			return pv_geoip2_get_strzval(msg, param, res, gpv->item->r.metro);
		case 13: /* nmask */
			if((gpv->item->r.flags & 1024) == 0) {
				gpv->item->r.nmask[0] = '\0';
				snprintf(gpv->item->r.nmask, 8, "%hd",
						gpv->item->r.record.netmask);
				gpv->item->r.flags |= 1024;
			}
			return pv_geoip2_get_strzval(msg, param, res, gpv->item->r.nmask);
		default: /* cc */
			if(gpv->item->r.country.s == NULL) {
				if(gpv->item->r.flags & 512)
					return pv_get_null(msg, param, res);
				if(get_mmdb_value2(&gpv->item->r.record.entry, &entry_data,
						   "country", "iso_code")
								!= MMDB_SUCCESS
						&& get_mmdb_value2(&gpv->item->r.record.entry,
								   &entry_data, "registered_country",
								   "iso_code")
								   != MMDB_SUCCESS)
					return pv_get_null(msg, param, res);
				if(entry_data.has_data
						&& entry_data.type == MMDB_DATA_TYPE_UTF8_STRING) {
					gpv->item->r.country.s = (char *)entry_data.utf8_string;
					gpv->item->r.country.len = entry_data.data_size;
				}
				if(get_mmdb_value2(&gpv->item->r.record.entry, &entry_data,
						   "traits", "is_anonymous_proxy")
								== MMDB_SUCCESS
						&& entry_data.has_data
						&& entry_data.type == MMDB_DATA_TYPE_BOOLEAN
						&& entry_data.boolean) {
					gpv->item->r.country.s = "A1";
					gpv->item->r.country.len = 2;
				}
				gpv->item->r.flags |= 512;
			}
			return pv_get_strval(msg, param, res, &gpv->item->r.country);
	}
}

static int init_shmlock(void)
{
	_sr_geoip2_reloadG = (int *)shm_malloc(sizeof(int));
	if(_sr_geoip2_reloadG == NULL) {
		LM_CRIT("cannot allocate memory for reload step\n");
		return -1;
	}
	*_sr_geoip2_reloadG = 1;
	_sr_geoip2_lock = lock_alloc();
	if(!_sr_geoip2_lock) {
		LM_CRIT("cannot allocate memory for lock\n");
		return -1;
	}
	if(lock_init(_sr_geoip2_lock) == 0) {
		LM_CRIT("cannot initialize lock\n");
		return -1;
	}

	return 0;
}

static void destroy_shmlock(void)
{
	if(_sr_geoip2_lock) {
		lock_destroy(_sr_geoip2_lock);
		lock_dealloc((void *)_sr_geoip2_lock);
		_sr_geoip2_lock = NULL;
	}
	if(_sr_geoip2_reloadG != NULL) {
		shm_free(_sr_geoip2_reloadG);
		_sr_geoip2_reloadG = NULL;
	}
}

int geoip2_init_pv(char *path)
{
	int status;

	_handle_GeoIP = (struct MMDB_s *)malloc(sizeof(struct MMDB_s));
	if(_handle_GeoIP == NULL) {
		SYS_MEM_ERROR;
		return -1;
	}

	memset(_handle_GeoIP, 0, sizeof(struct MMDB_s));
	status = MMDB_open(path, MMDB_MODE_MMAP, _handle_GeoIP);

	if(MMDB_SUCCESS != status) {
		LM_ERR("cannot open GeoIP database file at: %s\n", path);
		return -1;
	}

	if(init_shmlock() != 0) {
		LM_ERR("cannot create GeoIP database lock\n");
		return -1;
	}
	return 0;
}

int geoip2_reload_set(void)
{
	if(_sr_geoip2_reloadG == NULL) {
		LM_ERR("not initialized\n");
		return -1;
	}
	lock_get(_sr_geoip2_lock);
	*_sr_geoip2_reloadG = *_sr_geoip2_reloadG + 1;
	lock_release(_sr_geoip2_lock);

	return 0;
}

int geoip2_reload_pv(char *path)
{
	int status = 0;

	lock_get(_sr_geoip2_lock);
	if(*_sr_geoip2_reloadG == _sr_geoip2_reloadL) {
		lock_release(_sr_geoip2_lock);
		return MMDB_SUCCESS;
	}
	MMDB_close(_handle_GeoIP);
	status = MMDB_open(path, MMDB_MODE_MMAP, _handle_GeoIP);
	if(MMDB_SUCCESS != status) {
		LM_ERR("cannot reload GeoIP database file at: %s\n", path);
	} else {
		LM_INFO("reloaded GeoIP database file at: %s\n", path);
		_sr_geoip2_reloadL = *_sr_geoip2_reloadG;
	}
	lock_release(_sr_geoip2_lock);

	return status;
}

void geoip2_destroy_list(void)
{
}

void geoip2_destroy_pv(void)
{
	destroy_shmlock();
}

void geoip2_pv_reset(str *name)
{
	sr_geoip2_record_t *gr = NULL;

	gr = sr_geoip2_get_record(name);

	if(gr == NULL)
		return;
	memset(gr, 0, sizeof(struct _sr_geoip2_record));
}

int geoip2_update_pv(str *tomatch, str *name)
{
	sr_geoip2_record_t *gr = NULL;
	int gai_error, mmdb_error;

	if(tomatch->len > 255) {
		LM_ERR("target too long (max 255): %s\n", tomatch->s);
		return -3;
	}

	gr = sr_geoip2_get_record(name);
	if(gr == NULL) {
		LM_ERR("container not found: %s\n", tomatch->s);
		return -4;
	}

	strncpy(gr->tomatch, tomatch->s, tomatch->len);
	tomatch->s[tomatch->len] = '\0';
	lock_get(_sr_geoip2_lock);
	gr->record = MMDB_lookup_string(
			_handle_GeoIP, (const char *)gr->tomatch, &gai_error, &mmdb_error);
	lock_release(_sr_geoip2_lock);
	LM_DBG("attempt to match: %s\n", gr->tomatch);
	if(gai_error || MMDB_SUCCESS != mmdb_error || !gr->record.found_entry) {
		LM_DBG("no match for: %s\n", gr->tomatch);
		return -2;
	}
	LM_DBG("geoip2 PV updated for: %s\n", gr->tomatch);

	return 1;
}
