/*
 * Copyright (C) 2012 Andrew Mortensen
 *
 * This file is part of the sca module for Kamailio, a free SIP server.
 *
 * The sca module 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
 *
 * The sca module 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 "sca_common.h"

#include <assert.h>

#include "sca.h"
#include "sca_appearance.h"
#include "sca_call_info.h"
#include "sca_event.h"
#include "sca_notify.h"
#include "sca_util.h"

#include "../../modules/tm/tm_load.h"

const str SCA_METHOD_NOTIFY = STR_STATIC_INIT("NOTIFY");

static void sca_notify_reply_cb(
		struct cell *t, int cb_type, struct tmcb_params *cbp)
{
	struct sip_msg *notify_reply = NULL;
	str to_aor = STR_NULL;
	str *contact_uri;

	if(cbp == NULL) {
		LM_ERR("Empty parameters passed to NOTIFY callback!\n");
		return;
	}
	if((notify_reply = cbp->rpl) == NULL) {
		LM_ERR("Empty reply passed to NOTIFY callback!\n");
		return;
	}

	contact_uri = &t->uac[0].uri;
	if(notify_reply != FAKED_REPLY && REPLY_CLASS(notify_reply) == 2) {
		LM_DBG("NOTIFY %.*s returned %d\n", STR_FMT(contact_uri),
				notify_reply->REPLY_STATUS);
		return;
	}


	// after this, we've either gotten an error from the client, or a faked
	// reply from the proxy. remove the subscription in either case. it's
	// possible the client will return 481 (no such transaction), but that's
	// still grounds for us to remove the subscription, since the dialog
	// we have associated with the subscription is no longer valid.
	if(notify_reply == FAKED_REPLY) {
		LM_ERR("NOTIFY %.*s resulted in FAKED_REPLY from proxy: "
			   "failed to deliver NOTIFY to client\n",
				STR_FMT(contact_uri));
	} else {
		LM_ERR("NOTIFY %.*s returned %d %.*s removing call-info "
			   "subscription for %.*s\n",
				STR_FMT(contact_uri), notify_reply->REPLY_STATUS,
				STR_FMT(&notify_reply->first_line.u.reply.reason),
				STR_FMT(contact_uri));
	}
	return;

	if(sca_uri_extract_aor(&t->to, &to_aor) < 0) {
		LM_ERR("Failed to extract AoR from %.*s\n", STR_FMT(&t->to));
		return;
	}
	// t->to is the entire To header: "To: sip:....", so move to_aor.s ahead
	if(memcmp(to_aor.s, "To: ", strlen("To: ")) == 0) {
		to_aor.s += strlen("To: ");
		to_aor.len -= strlen("To: ");
	}

	if(sca_subscription_delete_subscriber_for_event(
			   sca, contact_uri, &SCA_EVENT_NAME_CALL_INFO, &to_aor)
			< 0) {
		LM_ERR("Failed to delete %.*s %.*s subscription\n",
				STR_FMT(contact_uri), STR_FMT(&SCA_EVENT_NAME_CALL_INFO));
	}
}

static dlg_t *sca_notify_dlg_for_subscription(sca_subscription *sub)
{
	dlg_t *dlg = NULL;

	dlg = (dlg_t *)pkg_malloc(sizeof(dlg_t));
	if(dlg == NULL) {
		LM_ERR("pkg_malloc dlg_t for %.*s failed: out of memory\n",
				STR_FMT(&sub->subscriber));
		goto error;
	}
	memset(dlg, 0, sizeof(dlg_t));

	dlg->loc_seq.value = sub->dialog.notify_cseq;
	dlg->loc_seq.is_set = 1;

	dlg->id.call_id = sub->dialog.call_id;
	dlg->id.rem_tag = sub->dialog.from_tag;
	dlg->id.loc_tag = sub->dialog.to_tag;

	// RURI
	dlg->rem_target = sub->subscriber;

	// To and From URIs are both the SCA AoR in an SCA NOTIFY
	dlg->loc_uri = sub->target_aor;
	dlg->rem_uri = sub->target_aor;

	// restore route
	if(!SCA_STR_EMPTY(&sub->rr)) {
		if(parse_rr_body(sub->rr.s, sub->rr.len, &dlg->route_set) < 0) {
			LM_ERR("sca_notify_dlg_for_subscription: failed to parse "
				   "%.*s subscription's Record-Route info\n",
					STR_FMT(&sub->subscriber));
			goto error;
		}
	}

	// the dialog state in an SCA NOTIFY should always be confirmed,
	// since we generated the dialog to-tag in our response to the
	// subscriber's SUBSCRIBE request.
	dlg->state = DLG_CONFIRMED;

	return (dlg);

error:
	if(dlg != NULL) {
		pkg_free(dlg);
	}

	return (NULL);
}

static int sca_notify_append_subscription_state_header(
		sca_subscription *sub, char *hdrbuf, int maxlen)
{
	str state_str = STR_NULL;
	int len, total = 0;
	int ttl = sub->expires - time(NULL);

	if(ttl < 0) {
		ttl = 0;
	}

	sca_subscription_state_to_str(sub->state, &state_str);
	len = snprintf(hdrbuf, maxlen, "Subscription-State: %s", state_str.s);
	if(len >= maxlen) {
		goto error;
	}
	total += len;

	if(ttl > 0) {
		len = snprintf(hdrbuf + total, maxlen - total, ";expires=%d", ttl);
		if(len >= maxlen) {
			goto error;
		}
		total += len;
	}

	len = snprintf(hdrbuf + total, maxlen - total, "%s", CRLF);
	total += len;

	return (total);

error:
	LM_ERR("Cannot append Subscription-State header: buffer too small\n");
	return (-1);
}

static int sca_notify_append_contact_header(
		sca_subscription *sub, char *hdrbuf, int maxlen)
{
	int len = strlen("Contact: ");
	str *orig = NULL;

	if(sca->cfg->server_address != NULL) {
		orig = sca->cfg->server_address;
	} else {
		orig = &sub->target_aor;
	}

	if(len + orig->len + strlen(CRLF) >= maxlen) {
		LM_ERR("Cannot append Contact header: buffer too small\n");
		return (-1);
	}

	memcpy(hdrbuf, "Contact: ", len);
	memcpy(hdrbuf + len, orig->s, orig->len);
	len += orig->len;
	memcpy(hdrbuf + len, CRLF, strlen(CRLF));
	len += strlen(CRLF);

	return (len);
}

static int sca_notify_build_headers_from_info(str *hdrs, int max_hdrs_len,
		sca_mod *scam, sca_subscription *sub, int app_idx)
{
	int len = 0;

	assert(hdrs != NULL);

	len = sca_notify_append_contact_header(sub, hdrs->s, max_hdrs_len);
	if(len < 0) {
		LM_ERR("Failed to add Contact header to %s NOTIFY for %.*s\n",
				sca_event_name_from_type(sub->event),
				STR_FMT(&sub->subscriber));
		goto error;
	}
	hdrs->len = len;

	if(app_idx == SCA_CALL_INFO_APPEARANCE_INDEX_ANY) {
		// add Call-Info header with appearance state
		if((len = sca_call_info_build_header(
					scam, sub, hdrs->s + hdrs->len, max_hdrs_len - hdrs->len))
				< 0) {
			LM_ERR("Failed to build Call-Info Headers for %s NOTIFY to %.*s\n",
					sca_event_name_from_type(sub->event),
					STR_FMT(&sub->subscriber));
			goto error;
		}
	} else {
		// just add Call-Info header with single appearance index
		len = sca_call_info_append_header_for_appearance_index(
				sub, app_idx, hdrs->s + hdrs->len, max_hdrs_len - hdrs->len);
		if(len < 0) {
			goto error;
		}
	}

	hdrs->len += len;

	len = sca_event_append_header_for_type(
			sub->event, hdrs->s + hdrs->len, max_hdrs_len - hdrs->len);
	if(len < 0) {
		LM_ERR("Failed to add Event header to %s NOTIFY for %.*s\n",
				sca_event_name_from_type(sub->event),
				STR_FMT(&sub->subscriber));
		goto error;
	}
	hdrs->len += len;

	len = sca_notify_append_subscription_state_header(
			sub, hdrs->s + hdrs->len, max_hdrs_len - hdrs->len);
	if(len < 0) {
		LM_ERR("Failed to add Subscription-State header to %s NOTIFY for "
			   "%.*s\n",
				sca_event_name_from_type(sub->event),
				STR_FMT(&sub->subscriber));
		goto error;
	}
	hdrs->len += len;

	return (hdrs->len);

error:
	return (-1);
}

static int sca_notify_subscriber_internal(
		sca_mod *scam, sca_subscription *sub, str *headers)
{
	uac_req_t request;
	dlg_t *dlg = NULL;
	str state_str = STR_NULL;
	int rc = -1;

	sca_appearance_state_to_str(sub->state, &state_str);
	LM_DBG("SCA: NOTIFYing subscriber '%.*s' of event '%s' with a state of "
		   "'%.*s' to index '%d'\n",
			STR_FMT(&sub->subscriber), sca_event_name_from_type(sub->event),
			STR_FMT(&state_str), sub->index);

	dlg = sca_notify_dlg_for_subscription(sub);
	if(dlg == NULL) {
		LM_ERR("Failed to create dlg_t for %s NOTIFY to %.*s\n",
				sca_event_name_from_type(sub->event),
				STR_FMT(&sub->subscriber));
		goto done;
	}

	set_uac_req(&request, (str *)&SCA_METHOD_NOTIFY, headers, NULL, dlg,
			TMCB_LOCAL_COMPLETED, sca_notify_reply_cb, scam);
	rc = scam->tm_api->t_request_within(&request);
	if(rc < 0) {
		LM_ERR("Failed to send in-dialog %s NOTIFY to %.*s\n",
				sca_event_name_from_type(sub->event),
				STR_FMT(&sub->subscriber));
	}
	// fall through, return rc from t_request_within

done:
	if(dlg != NULL) {
		if(dlg->route_set != NULL) {
			free_rr(&dlg->route_set);
		}

		pkg_free(dlg);
	}

	return (rc);
}

#define SCA_HEADERS_MAX_LEN 4096
int sca_notify_subscriber(sca_mod *scam, sca_subscription *sub, int app_idx)
{
	str headers = STR_NULL;
	char hdrbuf[SCA_HEADERS_MAX_LEN];

	LM_DBG("NOTIFYing subscriber because of a SUBSCRIPTION request\n");

	headers.s = hdrbuf;
	if(sca_notify_build_headers_from_info(
			   &headers, sizeof(hdrbuf), scam, sub, app_idx)
			< 0) {
		LM_ERR("Failed to build NOTIFY headers\n");
		return (-1);
	}

	return (sca_notify_subscriber_internal(scam, sub, &headers));
}

/*
 * send a call-info NOTIFY to all subscribers to a given SCA AoR.
 */
int sca_notify_call_info_subscribers(sca_mod *scam, str *subscription_aor)
{
	sca_hash_slot *slot;
	sca_hash_entry *e;
	sca_subscription *sub;
	str headers = STR_NULL;
	str hash_key = STR_NULL;
	char hdrbuf[SCA_HEADERS_MAX_LEN];
	char keybuf[512];
	char *event_name;
	int slot_idx;
	int rc = -1;

	assert(scam->subscriptions != NULL);
	assert(!SCA_STR_EMPTY(subscription_aor));

	LM_DBG("Notifying ALL subscribers of AOR %.*s due to a SUBSCRIBTION "
		   "request\n",
			STR_FMT(subscription_aor));

	event_name = sca_event_name_from_type(SCA_EVENT_TYPE_CALL_INFO);
	if(subscription_aor->len + strlen(event_name) >= sizeof(keybuf)) {
		LM_ERR("Hash key %.*s + %s is too long\n", STR_FMT(subscription_aor),
				event_name);
		return (-1);
	}
	hash_key.s = keybuf;
	SCA_STR_COPY(&hash_key, subscription_aor);
	SCA_STR_APPEND_CSTR(&hash_key, event_name);

	slot_idx = sca_hash_table_index_for_key(scam->subscriptions, &hash_key);
	slot = sca_hash_table_slot_for_index(scam->subscriptions, slot_idx);

	sca_hash_table_lock_index(scam->subscriptions, slot_idx);

	for(e = slot->entries; e != NULL; e = e->next) {
		sub = (sca_subscription *)e->value;
		if(!SCA_STR_EQ(subscription_aor, &sub->target_aor)) {
			continue;
		}

		if(headers.len == 0) {
			headers.s = hdrbuf;

			if(sca_notify_build_headers_from_info(&headers, sizeof(hdrbuf),
					   scam, sub, SCA_CALL_INFO_APPEARANCE_INDEX_ANY)
					< 0) {
				LM_ERR("Failed to build NOTIFY headers\n");
				goto done;
			}
		}

		// XXX would like this to be wrapped in one location
		sub->dialog.notify_cseq += 1;

		if(sca_notify_subscriber_internal(scam, sub, &headers) < 0) {
			goto done;
		}
	}
	rc = 1;

done:
	sca_hash_table_unlock_index(scam->subscriptions, slot_idx);

	return (rc);
}