/*
 * $Id$
 *
 * LDAP Database Driver for SER
 *
 * Copyright (C) 2008 iptelorg GmbH
 *
 * This file is part of SER, a free SIP server.
 *
 * SER 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.
 *
 * SER 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.
 */

/** \addtogroup ldap
 * @{
 */

/** \file
 * Implementation of functions related to database commands.
 */

#include "ld_cmd.h"
#include "ld_fld.h"
#include "ld_con.h"
#include "db2_ldap_mod.h"
#include "ld_uri.h"
#include "ld_cfg.h"
#include "ld_res.h"

#include "../../core/mem/mem.h"
#include "../../core/dprint.h"
#include "../../core/ut.h"

#include <string.h>


/** Destroys a ld_cmd structure.
 * This function frees all memory used by ld_cmd structure.
 * @param cmd A pointer to generic db_cmd command being freed.
 * @param payload A pointer to ld_cmd structure to be freed.
 */
static void ld_cmd_free(db_cmd_t *cmd, struct ld_cmd *payload)
{
	db_drv_free(&payload->gen);
	if(payload->result)
		pkg_free(payload->result);
	pkg_free(payload);
}


static int build_result_array(char ***res, db_cmd_t *cmd)
{
	struct ld_fld *lfld;
	char **t;
	int i;
	if(cmd->result_count == 0) {
		*res = NULL;
		return 0;
	}

	t = (char **)pkg_malloc(sizeof(char *) * (cmd->result_count + 1));
	if(t == NULL) {
		ERR("ldap: No memory left\n");
		return -1;
	}
	t[cmd->result_count] = NULL;

	for(i = 0; i < cmd->result_count; i++) {
		lfld = DB_GET_PAYLOAD(cmd->result + i);
		/* Attribute names are always zero terminated */
		t[i] = lfld->attr.s;
	}

	*res = t;
	return 0;
}


int ld_cmd(db_cmd_t *cmd)
{
	struct ld_cmd *lcmd;
	struct ld_cfg *cfg;
	struct ld_fld *lfld;
	int i, j;

	lcmd = (struct ld_cmd *)pkg_malloc(sizeof(struct ld_cmd));
	if(lcmd == NULL) {
		ERR("ldap: No memory left\n");
		goto error;
	}
	memset(lcmd, '\0', sizeof(struct ld_cmd));
	if(db_drv_init(&lcmd->gen, ld_cmd_free) < 0)
		goto error;

	switch(cmd->type) {
		case DB_PUT:
		case DB_DEL:
		case DB_UPD:
			ERR("ldap: The driver does not support directory modifications "
				"yet.\n");
			goto error;
			break;

		case DB_GET:
			break;

		case DB_SQL:
			ERR("ldap: The driver does not support raw queries yet.\n");
			goto error;
	}

	cfg = ld_find_cfg(&cmd->table);
	if(cfg == NULL) {
		ERR("ldap: Cannot find configuration for '%.*s', giving up\n",
				STR_FMT(&cmd->table));
		goto error;
	}

	lcmd->base = cfg->base.s;
	lcmd->scope = cfg->scope;

	lcmd->sizelimit = cfg->sizelimit;
	if(cfg->timelimit) {
		lcmd->timelimit.tv_sec = cfg->timelimit;
		lcmd->timelimit.tv_usec = 0;
	}

	if(cfg->filter.s) {
		lcmd->filter = cfg->filter;
	}
	lcmd->chase_references = cfg->chase_references;
	lcmd->chase_referrals = cfg->chase_referrals;

	if(ld_resolve_fld(cmd->match, cfg) < 0)
		goto error;
	if(ld_resolve_fld(cmd->result, cfg) < 0)
		goto error;

	/* prepare filter for each result field */
	for(i = 0; !DB_FLD_EMPTY(cmd->result) && !DB_FLD_LAST(cmd->result[i]);
			i++) {
		int n;
		lfld = DB_GET_PAYLOAD(cmd->result + i);
		lfld->filter = NULL;

		for(j = 0, n = 0;
				!DB_FLD_EMPTY(cmd->match) && !DB_FLD_LAST(cmd->match[j]); j++) {
			if(strcmp(cmd->result[i].name, cmd->match[j].name) == 0)
				n++;
		}

		if(n > 0) {
			lfld->filter = pkg_malloc((n + 1) * sizeof(*(lfld->filter)));
			if(!lfld->filter)
				return -1 /* E_OUT_OF_MEM*/;
			for(j = 0, n = 0;
					!DB_FLD_EMPTY(cmd->match) && !DB_FLD_LAST(cmd->match[j]);
					j++) {
				if(strcmp(cmd->result[i].name, cmd->match[j].name) == 0) {
					lfld->filter[n] = cmd->match + j;
					n++;
				}
			}
			lfld->filter[n] = NULL;
		}
	}
	if(build_result_array(&lcmd->result, cmd) < 0)
		goto error;

	DB_SET_PAYLOAD(cmd, lcmd);
	return 0;

error:
	if(lcmd) {
		DB_SET_PAYLOAD(cmd, NULL);
		db_drv_free(&lcmd->gen);
		if(lcmd->result)
			pkg_free(lcmd->result);
		pkg_free(lcmd);
	}
	return -1;
}


int ld_cmd_exec(db_res_t *res, db_cmd_t *cmd)
{
	db_con_t *con;
	struct ld_res *lres;
	struct ld_cmd *lcmd;
	struct ld_con *lcon;
	char *filter, *err_desc;
	int ret, err;
	LDAPMessage *msg, *resmsg;
	int reconn_cnt;
	int msgid;
	char *oid;
	struct berval *data;
	struct timeval restimeout;

	filter = NULL;
	err_desc = NULL;
	resmsg = NULL;

	/* First things first: retrieve connection info from the currently active
	 * connection and also mysql payload from the database command
	 */
	con = cmd->ctx->con[db_payload_idx];
	lcmd = DB_GET_PAYLOAD(cmd);
	lcon = DB_GET_PAYLOAD(con);

	reconn_cnt = ld_reconnect_attempt;

	if(ld_prepare_ldap_filter(&filter, cmd, &lcmd->filter) < 0) {
		ERR("ldap: Error while building LDAP search filter\n");
		goto error;
	}

	DBG("ldap: ldap_search(base:'%s', filter:'%s')\n", lcmd->base, filter);
	do {
		if(lcon->flags & LD_CONNECTED) {
			ldap_set_option(lcon->con, LDAP_OPT_DEREF,
					((void *)&lcmd->chase_references));
			/* there is alternative method using LDAP_CONTROL_REFERRALS per request but is not well documented */
			ldap_set_option(lcon->con, LDAP_OPT_REFERRALS,
					lcmd->chase_referrals ? LDAP_OPT_ON : LDAP_OPT_OFF);

			ret = ldap_search_ext(lcon->con, lcmd->base, lcmd->scope, filter,
					lcmd->result, 0, NULL, NULL,
					lcmd->timelimit.tv_sec ? &lcmd->timelimit : NULL,
					lcmd->sizelimit, &msgid);
			if(ret != LDAP_SUCCESS) {
				ERR("ldap: Error while searching: %s\n", ldap_err2string(ret));
				goto error;
			}

			/*
			 openldap v2.3 library workaround for unsolicited messages:
			 if only unsolicited messages are available then ldap_result of
			 v2.3 library waits forever
			*/
			memset(&restimeout, 0, sizeof(restimeout));
			restimeout.tv_sec = 5;
			ret = ldap_result(lcon->con, LDAP_RES_ANY, LDAP_MSG_ALL,
					&restimeout, &resmsg);
		} else {
			/* force it to reconnect */
			ret = -1;
		}

		if(ret <= 0) {
			ERR("ldap: Error in ldap_search: %s\n",
					ret < 0 ? ldap_err2string(ret) : "timeout");
			if(ret == LDAP_SERVER_DOWN) {
				lcon->flags &= ~LD_CONNECTED;
				do {
					if(!reconn_cnt) {
						ERR("ldap: maximum reconnection attempt reached! "
							"giving up\n");
						goto error;
					}
					reconn_cnt--;
					err = ld_con_connect(con);
				} while(err != 0);
			} else {
				goto error;
			}
		}
	} while(ret <= 0);

	/* looking for unsolicited messages */
	for(msg = ldap_first_message(lcon->con, resmsg); msg != NULL;
			msg = ldap_next_message(lcon->con, msg)) {
		if(ldap_msgtype(msg) == LDAP_RES_EXTENDED) {
			if(ldap_parse_extended_result(lcon->con, msg, &oid, &data, 0)
					!= LDAP_SUCCESS) {
				ERR("ldap: Error while parsing extended result\n");
				goto error;
			}
			if(oid != NULL) {
				if(strcmp(oid, LDAP_NOTICE_OF_DISCONNECTION) == 0) {
					WARN("ldap: Notice of Disconnection (OID: %s)\n", oid);
				} else {
					WARN("ldap: Unsolicited message received. OID: %s\n", oid);
				}
				ldap_memfree(oid);
			}
			if(data != NULL) {
				WARN("ldap: Unsolicited message data: %.*s\n",
						(int)data->bv_len, data->bv_val);
				ber_bvfree(data);
			}
		}
	}

	ret = ldap_parse_result(
			lcon->con, resmsg, &err, NULL, &err_desc, NULL, NULL, 0);
	if(ret != LDAP_SUCCESS) {
		ERR("ldap: Error while reading result status: %s\n",
				ldap_err2string(ret));
		goto error;
	}

	if(err != LDAP_SUCCESS) {
		ERR("ldap: LDAP server reports error: %s\n", ldap_err2string(err));
		goto error;
	}

	if(res) {
		lres = DB_GET_PAYLOAD(res);
		lres->msg = resmsg;
	} else if(resmsg) {
		ldap_msgfree(resmsg);
	}

	if(filter)
		pkg_free(filter);
	if(err_desc)
		ldap_memfree(err_desc);
	return 0;

error:
	if(filter)
		pkg_free(filter);
	if(resmsg)
		ldap_msgfree(resmsg);
	if(err_desc)
		ldap_memfree(err_desc);
	return -1;
}


/* Iterate to the next search result in the linked list
 * of messages returned by the LDAP server and convert
 * the field values.
 */
static int search_entry(db_res_t *res, int init)
{
	db_con_t *con;
	struct ld_res *lres;
	struct ld_con *lcon;
	int r;
	lres = DB_GET_PAYLOAD(res);
	/* FIXME */
	con = res->cmd->ctx->con[db_payload_idx];
	lcon = DB_GET_PAYLOAD(con);

	if(init || !lres->current
			|| ldap_msgtype(lres->current) != LDAP_RES_SEARCH_ENTRY
			/* there is no more value combination result left */
			|| ld_incindex(res->cmd->result)) {

		do {
			if(init) {
				lres->current = ldap_first_message(lcon->con, lres->msg);
				init = 0;
			} else
				lres->current = ldap_next_message(lcon->con, lres->current);

			while(lres->current) {
				if(ldap_msgtype(lres->current) == LDAP_RES_SEARCH_ENTRY) {
					break;
				}
				lres->current = ldap_next_message(lcon->con, lres->current);
			}
			if(lres->current == NULL)
				return 1;
			r = ld_ldap2fldinit(res->cmd->result, lcon->con, lres->current);
		} while(r > 0);
		if(r < 0)
			return -1;
	} else {
		if(ld_ldap2fld(res->cmd->result, lcon->con, lres->current) < 0)
			return -1;
	}

	res->cur_rec->fld = res->cmd->result;
	return 0;
}


int ld_cmd_first(db_res_t *res)
{
	return search_entry(res, 1);
}


int ld_cmd_next(db_res_t *res)
{
	return search_entry(res, 0);
}

#define is_space(c)                                                       \
	((c) == ' ' || (c) == ',' || (c) == ';' || (c) == '\t' || (c) == '\n' \
			|| (c) == '\r' || (c) == '\0')

int ld_cmd_setopt(db_cmd_t *cmd, char *optname, va_list ap)
{
	struct ld_fld *lfld;
	char *val, *c;
	int i;

	if(!strcasecmp("client_side_filtering", optname)) {
		val = va_arg(ap, char *);

		for(i = 0; !DB_FLD_EMPTY(cmd->result) && !DB_FLD_LAST(cmd->result[i]);
				i++) {
			c = val;
			do {
				c = strstr(c, cmd->result[i].name);
				if(c) {
					if((c == val || is_space(*(c - 1)))
							&& is_space(*(c + strlen(cmd->result[i].name)))) {
						lfld = (struct ld_fld *)DB_GET_PAYLOAD(cmd->result + i);
						lfld->client_side_filtering = 1;
						break;
					}
					c += strlen(cmd->result[i].name);
				}
			} while(c != NULL);
		}
	} else
		return 1;
	return 0;
}

/** @} */
