/*
 * This file is part of the Sofia-SIP package
 *
 * Copyright (C) 2006 Nokia Corporation.
 *
 * Contact: Pekka Pessi <pekka.pessi@nokia.com>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public License
 * as published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA
 *
 */

/**@CFILE nua_notifier.c
 * @brief SUBSCRIBE server, NOTIFY client and REFER server
 *
 * Simpler event server. See nua_event_server.c for more complex event
 * server.
 *
 * @author Pekka Pessi <Pekka.Pessi@nokia.com>
 *
 * @date Created: Wed Mar  8 15:10:08 EET 2006 ppessi
 */

#include "config.h"

#include <stddef.h>
#include <stdlib.h>
#include <string.h>
#include <limits.h>

#include <assert.h>

#include <sofia-sip/string0.h>
#include <sofia-sip/sip_protos.h>
#include <sofia-sip/sip_status.h>
#include <sofia-sip/sip_util.h>
#include <sofia-sip/su_uniqueid.h>

#define NTA_LEG_MAGIC_T      struct nua_handle_s
#define NTA_OUTGOING_MAGIC_T struct nua_handle_s

#include "nua_stack.h"

/* ---------------------------------------------------------------------- */
/* Notifier event usage */

struct notifier_usage
{
  enum nua_substate  nu_substate;	/**< Subscription state */
  sip_time_t         nu_expires;
};

static char const *nua_notify_usage_name(nua_dialog_usage_t const *du);
static int nua_notify_usage_add(nua_handle_t *nh, 
				   nua_dialog_state_t *ds,
				   nua_dialog_usage_t *du);
static void nua_notify_usage_remove(nua_handle_t *nh, 
				       nua_dialog_state_t *ds,
				       nua_dialog_usage_t *du);
static void nua_notify_usage_refresh(nua_handle_t *nh,
				     nua_dialog_state_t *ds,
				     nua_dialog_usage_t *du,
				     sip_time_t now);
static int nua_notify_usage_shutdown(nua_handle_t *nh,
				     nua_dialog_state_t *ds,
				     nua_dialog_usage_t *du);

static nua_usage_class const nua_notify_usage[1] = {
  {
    sizeof (struct notifier_usage), (sizeof nua_notify_usage),
    nua_notify_usage_add,
    nua_notify_usage_remove,
    nua_notify_usage_name,
    NULL,
    nua_notify_usage_refresh,
    nua_notify_usage_shutdown,
  }};

static char const *nua_notify_usage_name(nua_dialog_usage_t const *du)
{
  return "notify";
}

static 
int nua_notify_usage_add(nua_handle_t *nh, 
			   nua_dialog_state_t *ds,
			   nua_dialog_usage_t *du)
{
  ds->ds_has_events++;
  ds->ds_has_notifys++;
  return 0;
}

static 
void nua_notify_usage_remove(nua_handle_t *nh, 
			       nua_dialog_state_t *ds,
			       nua_dialog_usage_t *du)
{
  ds->ds_has_events--;	
  ds->ds_has_notifys--;	
}

/* ====================================================================== */
/* SUBSCRIBE server */

static int respond_to_subscribe(nua_server_request_t *sr, tagi_t const *tags);

/** @NUA_EVENT nua_i_subscribe
 *
 * Incoming @b SUBSCRIBE request.
 *
 * @b SUBSCRIBE request is used to query SIP event state or establish a SIP
 * event subscription.
 *
 * Initial SUBSCRIBE requests are dropped with <i>489 Bad Event</i>
 * response, unless the application has explicitly included the @Event in
 * the list of allowed events with nua_set_params() tag NUTAG_ALLOW_EVENTS()
 * (or SIPTAG_ALLOW_EVENTS() or SIPTAG_ALLOW_EVENTS_STR()). The application
 * can decide whether to accept the SUBSCRIBE request or reject it. The
 * nua_response() call responding to a SUBSCRIBE request must have
 * NUTAG_WITH() (or NUTAG_WITH_CURRENT()/NUTAG_WITH_SAVED()) tag.
 *
 * If the application accepts the SUBSCRIBE request, it must immediately
 * send an initial NOTIFY establishing the dialog. This is because the
 * response to the SUBSCRIBE request may be lost because the SUBSCRIBE
 * request was forked by an intermediate proxy. 
 *
 * SUBSCRIBE requests modifying (usually refreshing or terminating) an
 * existing event subscription are accepted by default and a <i>200 OK</i>
 * response along with a copy of previously sent NOTIFY is sent
 * automatically.
 *
 * By default, only event subscriptions accepted are those created
 * implicitly by REFER request. See #nua_i_refer how the application must
 * handle the REFER requests.
 *
 * @param status status code of response sent automatically by stack
 * @param phrase response phrase sent automatically by stack
 * @param nh     operation handle associated with the incoming request
 * @param hmagic application context associated with the handle
 *               (NULL when handle is created by the stack)
 * @param sip    SUBSCRIBE request headers
 * @param tags   NUTAG_SUBSTATE()
 *
 * @sa @RFC3265, nua_notify(), NUTAG_SUBSTATE(), @SubscriptionState,
 * @Event, nua_subscribe(), #nua_r_subscribe, #nua_i_refer, nua_refer()
 *
 * @END_NUA_EVENT
 */


/** @internal Process incoming SUBSCRIBE. */
int nua_stack_process_subscribe(nua_t *nua,
				nua_handle_t *nh,
				nta_incoming_t *irq,
				sip_t const *sip)
{
  nua_server_request_t *sr, sr0[1];
  nua_dialog_state_t *ds;
  nua_dialog_usage_t *du = NULL;
  sip_event_t *o = sip->sip_event;
  char const *event = o ? o->o_type : NULL;
  
  enum nua_substate substate = nua_substate_terminated;

  enter;

  if (nh)
    du = nua_dialog_usage_get(ds = nh->nh_ds, nua_notify_usage, o);

  sr = SR_INIT(sr0);
  
  if (nh == NULL || du == NULL) {
    sip_allow_events_t *allow_events = NUA_PGET(nua, nh, allow_events);

    if (event && str0cmp(event, "refer") == 0)
      /* refer event subscription should be initiated with REFER */
      SR_STATUS1(sr, SIP_403_FORBIDDEN);
    else if (!event || !msg_header_find_param(allow_events->k_common, event))
      SR_STATUS1(sr, SIP_489_BAD_EVENT);
    else
      substate = nua_substate_embryonic;
  }
  else {
    /* Refresh existing subscription */
    struct notifier_usage *nu = nua_dialog_usage_private(du);
    unsigned long expires;

    assert(nh && du && nu);

    expires = str0cmp(event, "refer") ? 3600 : NH_PGET(nh, refer_expires);

    if (sip->sip_expires && sip->sip_expires->ex_delta < expires)
      expires = sip->sip_expires->ex_delta;

    if (expires == 0)
      nu->nu_substate = nua_substate_terminated;

    nu->nu_expires = sip_now() + expires;
    substate = nu->nu_substate;

    /* XXX - send notify */

    SR_STATUS1(sr, SIP_200_OK);
  }

  sr = nua_server_request(nua, nh, irq, sip, sr, sizeof *sr,
			  respond_to_subscribe, 1);

  if (!du && substate == nua_substate_embryonic && sr->sr_status < 300) {
    nh = sr->sr_owner; assert(nh && nh != nua->nua_dhandle);
    du = nua_dialog_usage_add(nh, nh->nh_ds, nua_notify_usage, sip->sip_event);
    if (du) {
      struct notifier_usage *nu = nua_dialog_usage_private(du);
      unsigned long expires = 3600; /* XXX */
      
      if (sip->sip_expires && sip->sip_expires->ex_delta < expires)
	expires = sip->sip_expires->ex_delta;

      nu->nu_expires = sip_now() + expires;
      nu->nu_substate = substate;
    }
    else 
      SR_STATUS1(sr, SIP_500_INTERNAL_SERVER_ERROR);
  }

  if (substate == nua_substate_embryonic && sr->sr_status >= 300)
    substate = nua_substate_terminated;

  sr->sr_usage = du;

  return nua_stack_server_event(nua, sr, nua_i_subscribe,
				NUTAG_SUBSTATE(substate), TAG_END());
}

/** @internal Respond to an SUBSCRIBE request.
 *
 */
static
int respond_to_subscribe(nua_server_request_t *sr, tagi_t const *tags)
{
  nua_handle_t *nh = sr->sr_owner;
  nua_dialog_state_t *ds = nh->nh_ds;
  nua_t *nua = nh->nh_nua;
  struct notifier_usage *nu;
  sip_allow_events_t *allow_events = NUA_PGET(nua, nh, allow_events);
  sip_expires_t ex[1]; 
  sip_time_t now = sip_now();
  msg_t *msg;

  sip_expires_init(ex);

  nu = nua_dialog_usage_private(sr->sr_usage);
  if (nu && nu->nu_expires > now)
    ex->ex_delta = nu->nu_expires - now;

  msg = nua_server_response(sr,
			    sr->sr_status, sr->sr_phrase,
			    NUTAG_ADD_CONTACT(sr->sr_status < 300),
			    TAG_IF(nu, SIPTAG_EXPIRES(ex)),
			    SIPTAG_SUPPORTED(NH_PGET(nh, supported)),
			    SIPTAG_ALLOW_EVENTS(allow_events),
			    TAG_NEXT(tags));

  if (msg) {
    sip_t *sip = sip_object(msg);

    if (nu && sip->sip_expires && sr->sr_status < 300)
      nu->nu_expires = now + sip->sip_expires->ex_delta;

    nta_incoming_mreply(sr->sr_irq, msg);

    if (nu && nu->nu_substate != nua_substate_embryonic)
      /* Send NOTIFY (and terminate subscription, when needed) */
      nua_dialog_usage_refresh(nh, ds, sr->sr_usage, sip_now());
  }
  else {
    /* XXX - send nua_i_error */
    SR_STATUS1(sr, SIP_500_INTERNAL_SERVER_ERROR);
    nta_incoming_treply(sr->sr_irq, sr->sr_status, sr->sr_phrase, TAG_END());
  }
  
  return sr->sr_status >= 200 ? sr->sr_status : 0;
}

/* ======================================================================== */
/* NOTIFY */

static int process_response_to_notify(nua_handle_t *nh,
				      nta_outgoing_t *orq,
				      sip_t const *sip);

static int nua_stack_notify2(nua_t *, nua_handle_t *, nua_event_t, 
			     nua_dialog_usage_t *du,
			     tagi_t const *tags);


/**@fn void nua_notify(nua_handle_t *nh, tag_type_t tag, tag_value_t value, ...);
 *
 * Send a SIP NOTIFY request message.
 *
 * This function is used when the application implements itself the
 * notifier. The application must provide valid @SubscriptionState and
 * @Event headers using SIP tags. If there is no @SubscriptionState header,
 * the subscription state can be modified with NUTAG_SUBSTATE().
 *
 * @bug If the @Event is not given by application, stack uses the @Event
 * header from the first subscription usage on handle.
 *
 * @param nh              Pointer to operation handle
 * @param tag, value, ... List of tagged parameters
 *
 * @return 
 *    nothing
 *
 * @par Related Tags:
 *    NUTAG_SUBSTATE() \n
 *    Tags of nua_set_hparams() \n
 *    Tags in <sip_tag.h>
 *
 * @par Events:
 *    #nua_r_notify
 *
 * @sa @RFC3265, #nua_i_subscribe, #nua_i_refer, NUTAG_ALLOW_EVENTS()
 */

/**@internal Send NOTIFY. */
int nua_stack_notify(nua_t *nua,
		     nua_handle_t *nh,
		     nua_event_t e,
		     tagi_t const *tags)
{
  return nua_stack_notify2(nua, nh, e, NULL, tags);
}


int nua_stack_notify2(nua_t *nua,
		      nua_handle_t *nh,
		      nua_event_t e,
		      nua_dialog_usage_t *du,
		      tagi_t const *tags)
{
  nua_client_request_t *cr = nh->nh_ds->ds_cr;
  struct notifier_usage *nu;
  msg_t *msg;
  sip_t *sip;
  sip_event_t const *o;
  sip_time_t now;
  int refresh = du != NULL;

  if (cr->cr_orq) {
    return UA_EVENT2(e, 900, "Request already in progress");
  }

  nua_stack_init_handle(nua, nh, TAG_NEXT(tags));

  if (refresh) {
    assert(!cr->cr_msg);
    if (cr->cr_msg)
      msg_destroy(cr->cr_msg);
    cr->cr_msg = msg_copy(du->du_msg);
  }

  msg = nua_creq_msg(nua, nh, cr, cr->cr_retry_count || refresh,
		     SIP_METHOD_NOTIFY,
		     NUTAG_ADD_CONTACT(1),
		     TAG_NEXT(tags));
  sip = sip_object(msg);
  if (!sip)
    return UA_EVENT1(e, NUA_INTERNAL_ERROR);

  if (nh->nh_ds->ds_has_notifys == 1 && !sip->sip_event)
    o = NONE;
  else
    o = sip->sip_event;

  du = nua_dialog_usage_get(nh->nh_ds, nua_notify_usage, o);
  nu = nua_dialog_usage_private(du);

  if (du && du->du_event && !sip->sip_event)
    sip_add_dup(msg, sip, (sip_header_t *)du->du_event);

  now = sip_now();

  if (!du)
    ;
  else if (sip->sip_subscription_state) {
    /* SIPTAG_SUBSCRIPTION_STATE() overrides NUTAG_SUBSTATE() */
    char const *ss_substate = sip->sip_subscription_state->ss_substate;

    if (strcasecmp(ss_substate, "terminated") == 0)
      nu->nu_substate = nua_substate_terminated;
    else if (strcasecmp(ss_substate, "pending") == 0)
      nu->nu_substate = nua_substate_pending;
    else /* if (strcasecmp(subs->ss_substate, "active") == 0) */ 
      nu->nu_substate = nua_substate_active;

    if (sip->sip_subscription_state->ss_expires) {
      unsigned long expires;
      expires = strtoul(sip->sip_subscription_state->ss_expires, NULL, 10);
      if (expires > 3600)
        expires = 3600;
      nu->nu_expires = now + expires;
    }
    else if (nu->nu_substate != nua_substate_terminated) {
      sip_subscription_state_t *ss = sip->sip_subscription_state;
      char *param;

      if (now < nu->nu_expires)
        param = su_sprintf(msg_home(msg), "expires=%lu", nu->nu_expires - now);
      else
        param = "expires=0";

      msg_header_add_param(msg_home(msg), ss->ss_common, param);
    }
  }
  else {
    sip_subscription_state_t *ss;
    enum nua_substate substate;
    char const *name;

    substate = nu->nu_substate;

    if (nu->nu_expires <= now)
      substate = nua_substate_terminated;

    if (substate != nua_substate_terminated) {
      tagi_t const *t = tl_find_last(tags, nutag_substate);
      if (t)
	substate = (enum nua_substate)t->t_value;
    }

    switch (substate) {
    case nua_substate_embryonic:
      /*FALLTHROUGH*/
    case nua_substate_pending:
      name = "pending";
      nu->nu_substate = nua_substate_pending;
      break;
    case nua_substate_active:
    default:
      name = "active";
      nu->nu_substate = nua_substate_active;
      break;
    case nua_substate_terminated:
      name = "terminated";
      nu->nu_substate = nua_substate_terminated;
      break;
    }

    if (nu->nu_substate != nua_substate_terminated) {
      unsigned long expires = nu->nu_expires - now;
      ss = sip_subscription_state_format(msg_home(msg), "%s;expires=%lu",
					 name, expires);
    }
    else {
      ss = sip_subscription_state_make(msg_home(msg), "terminated; "
				       "reason=noresource");
    }

    msg_header_insert(msg, (void *)sip, (void *)ss);
  }

  if (du) {
    if (nu->nu_substate == nua_substate_terminated)
      du->du_terminating = 1;

    if (!du->du_terminating && !refresh) {
      /* Save template */
      if (du->du_msg)
        msg_destroy(du->du_msg);
      du->du_msg = msg_ref_create(cr->cr_msg);
    }
  }

  /* NOTIFY outside a dialog */
  cr->cr_orq = nta_outgoing_mcreate(nua->nua_nta,
				    process_response_to_notify, nh, NULL,
				    msg,
				    SIPTAG_END(), TAG_NEXT(tags));

  if (!cr->cr_orq) {
    msg_destroy(msg);
    return UA_EVENT1(e, NUA_INTERNAL_ERROR);
  }

  cr->cr_usage = du;

  return cr->cr_event = e;
}

static
void restart_notify(nua_handle_t *nh, tagi_t *tags)
{
  nua_creq_restart(nh, nh->nh_ds->ds_cr, process_response_to_notify, tags);
}

/** @NUA_EVENT nua_r_notify
 *
 * Response to an outgoing @b NOTIFY request.
 *
 * The @b NOTIFY may be sent explicitly by nua_notify() or implicitly by NUA
 * state machine. Implicit @b NOTIFY is sent when an established dialog is
 * refreshed by client or it is terminated (either by client or because of a
 * timeout)
 *
 * @param status response status code
 *               (if the request is retried, @a status is 100, the @a
 *               sip->sip_status->st_status contain the real status code
 *               from the response message, e.g., 302, 401, or 407)
 * @param phrase a short textual description of @a status code
 * @param nh     operation handle associated with the subscription
 * @param hmagic application context associated with the handle
 * @param sip    response to @b NOTIFY request or NULL upon an error 
 *               (status code is in @a status and 
 *               descriptive message in @a phrase parameters)
 * @param tags   NUTAG_SUBSTATE() indicating subscription state
 *
 * @sa nua_notify(), @RFC3265, #nua_i_subscribe, #nua_i_refer
 *
 * @END_NUA_EVENT
 */

static int process_response_to_notify(nua_handle_t *nh,
				      nta_outgoing_t *orq,
				      sip_t const *sip)
{
  enum nua_substate substate = nua_substate_terminated;

  if (nua_creq_check_restart(nh, nh->nh_ds->ds_cr, orq, sip, restart_notify))
    return 0;

  if (nh->nh_ds->ds_cr->cr_usage) {
    struct notifier_usage *nu = nua_dialog_usage_private(nh->nh_ds->ds_cr->cr_usage);
    substate = nu->nu_substate;
    assert(substate != nua_substate_embryonic);
  }

  return nua_stack_process_response(nh, nh->nh_ds->ds_cr, orq, sip, 
				    NUTAG_SUBSTATE(substate),
				    TAG_END());
}


static void nua_notify_usage_refresh(nua_handle_t *nh,
				     nua_dialog_state_t *ds,
				     nua_dialog_usage_t *du,
				     sip_time_t now)
{
  struct notifier_usage *nu = nua_dialog_usage_private(du);

  if (nh->nh_ds->ds_cr->cr_usage == du) /* Already notifying. */
    return;

  if (now >= nu->nu_expires) {
    sip_subscription_state_t ss[1];
    char const *params[] = { NULL, NULL };
    tagi_t tags[2] = {
      { SIPTAG_SUBSCRIPTION_STATE(ss) }, { TAG_END() }
    };

    sip_subscription_state_init(ss);

    ss->ss_substate = "terminated";
    ss->ss_params = params;
    params[0] = "reason=timeout";
    ss->ss_reason = "timeout";

    nua_stack_notify2(nh->nh_nua, nh, nua_r_notify, du, tags);
  }
  else {
    nua_stack_notify2(nh->nh_nua, nh, nua_r_notify, du, NULL);
  }
}

/** @interal Shut down NOTIFY usage. 
 *
 * @retval >0  shutdown done
 * @retval 0   shutdown in progress
 * @retval <0  try again later
 */
static int nua_notify_usage_shutdown(nua_handle_t *nh,
				     nua_dialog_state_t *ds,
				     nua_dialog_usage_t *du)
{
  nua_client_request_t *cr = nh->nh_ds->ds_cr;

  if (!cr->cr_usage) {
    /* Unnotify */
	/* Commenting this line out to supress an attended transfer bug (awaiting fix from pessi) */
    //nua_stack_notify2(nh->nh_nua, nh, nua_r_destroy, du, NULL);
    return cr->cr_usage != du;
  }

  if (!du->du_ready && !cr->cr_orq)
    return 1;			/* Unauthenticated NOTIFY? */

  return -1;  /* Request in progress */
}


/* ======================================================================== */
/* REFER */
/* RFC 3515 */

/** @NUA_EVENT nua_i_refer
 *
 * Incoming @b REFER request used to transfer calls.
 *
 * @param status status code of response sent automatically by stack
 * @param phrase a short textual description of @a status code
 * @param nh     operation handle associated with the incoming request
 * @param hmagic application context associated with the handle
 *               (NULL if outside of an already established session)
 * @param sip    incoming REFER request
 * @param tags   NUTAG_REFER_EVENT() \n
 *               SIPTAG_REFERRED_BY()
 *  
 * @sa nua_refer(), #nua_r_refer, @ReferTo, NUTAG_REFER_EVENT(), 
 * SIPTAG_REFERRED_BY(), @ReferredBy, NUTAG_NOTIFY_REFER(),
 * NUTAG_REFER_WITH_ID(), @RFC3515.
 *
 * @END_NUA_EVENT
 */

/** @internal Process incoming REFER. */
int nua_stack_process_refer(nua_t *nua,
			    nua_handle_t *nh,
			    nta_incoming_t *irq,
			    sip_t const *sip)
{
  nua_dialog_usage_t *du = NULL;
  struct notifier_usage *nu;
  sip_event_t *event;
  sip_referred_by_t *by = NULL, default_by[1];
  msg_t *response;
  sip_time_t expires;
  int created = 0;

  if (nh == NULL) {
    if (!(nh = nua_stack_incoming_handle(nua, irq, sip, 1)))
      return 500;
    created = 1;
  }

  if (nh->nh_ds->ds_has_referrals || NH_PGET(nh, refer_with_id))
    event = sip_event_format(nh->nh_home, "refer;id=%u", sip->sip_cseq->cs_seq);
  else
    event = sip_event_make(nh->nh_home, "refer");

  if (event)
    du = nua_dialog_usage_add(nh, nh->nh_ds, nua_notify_usage, event);

  if (!du || du->du_ready) {
    if (du->du_ready) {
      SU_DEBUG_1(("nua(%p): REFER with existing refer;id=%u\n", nh,
		  sip->sip_cseq->cs_seq));
    }
    if (created) 
      nh_destroy(nua, nh);
    return 500;
  }

  nu = nua_dialog_usage_private(du);
  du->du_ready = 1;
  nh->nh_ds->ds_has_referrals = 1;

  nua_dialog_uas_route(nh, nh->nh_ds, sip, 1);	/* Set route and tags */

  if (!sip->sip_referred_by) {
    sip_from_t *a = sip->sip_from;

    sip_referred_by_init(by = default_by);

    *by->b_url = *a->a_url;
    by->b_display = a->a_display;
  }

  response = nh_make_response(nua, nh, irq, 
			      SIP_202_ACCEPTED, 
			      NUTAG_ADD_CONTACT(1),
			      TAG_END());

  nta_incoming_mreply(irq, response);

  expires = NH_PGET(nh, refer_expires);

  if (sip->sip_expires && sip->sip_expires->ex_delta < expires)
    expires = sip->sip_expires->ex_delta;
  nu->nu_substate = nua_substate_pending;
  nu->nu_expires = sip_now() + expires;

  /* Immediate notify in order to establish the dialog */
  if (!sip->sip_to->a_tag)
    nua_stack_post_signal(nh,
			  nua_r_notify,
			  SIPTAG_EVENT(event),
			  SIPTAG_CONTENT_TYPE_STR("message/sipfrag"),
			  SIPTAG_PAYLOAD_STR("SIP/2.0 100 Trying\r\n"),
			  TAG_END());
  
  nua_stack_event(nh->nh_nua, nh, nta_incoming_getrequest(irq),
		  nua_i_refer, SIP_202_ACCEPTED, 
		  NUTAG_REFER_EVENT(event),
		  TAG_IF(by, SIPTAG_REFERRED_BY(by)),
		  TAG_END());
  
  su_free(nh->nh_home, event);

  return 500;   
}
