/*
 * ser osp module. 
 *
 * This module enables ser to communicate with an Open Settlement 
 * Protocol (OSP) server.  The Open Settlement Protocol is an ETSI 
 * defined standard for Inter-Domain VoIP pricing, authorization
 * and usage exchange.  The technical specifications for OSP 
 * (ETSI TS 101 321 V4.1.1) are available at www.etsi.org.
 *
 * Uli Abend was the original contributor to this module.
 * 
 * Copyright (C) 2001-2005 Fhg Fokus
 *
 * 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
 *
 * For a license to use the ser software under conditions
 * other than those described here, or to purchase support for this
 * software, please contact iptel.org by e-mail at the following addresses:
 *    info@iptel.org
 *
 * 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

#include <osp/osp.h>
#include "../../sr_module.h"
#include "../../usr_avp.h"
#include "usage.h"
#include "destination.h"
#include "osptoolkit.h"
#include "sipheader.h"

#define OSP_ORIG_COOKIE     "osp-o"
#define OSP_TERM_COOKIE     "osp-t"

#define OSP_RELEASE_ORIG    0
#define OSP_RELEASE_TERM    1

/* The up case tags for the destinations may corrupt OSP cookies */
#define OSP_COOKIE_TRANSID      't'
#define OSP_COOKIE_TRANSIDUP    'T'
#define OSP_COOKIE_SRCIP        's'
#define OSP_COOKIE_SRCIPUP      'S'
#define OSP_COOKIE_AUTHTIME     'a'
#define OSP_COOKIE_AUTHTIMEUP   'A'
#define OSP_COOKIE_DSTCOUNT     'c'
#define OSP_COOKIE_DSTCOUNTUP   'C'

/* SER uses AVP to add RR pararmters */
const str OSP_ORIGCOOKIE_NAME = {"_osp_orig_cookie_", 17};
const str OSP_TERMCOOKIE_NAME = {"_osp_term_cookie_", 17};

extern char* _osp_device_ip;
extern OSPTPROVHANDLE _osp_provider;
extern str OSP_ORIGDEST_NAME;

static void ospRecordTransaction(struct sip_msg* msg, unsigned long long transid, char* uac, char* from, char* to, time_t authtime, int isorig, unsigned destinationCount);
static int ospBuildUsageFromDestination(OSPTTRANHANDLE transaction, osp_dest* dest, int lastcode);
static int ospReportUsageFromDestination(OSPTTRANHANDLE transaction, osp_dest* dest);
/* SER checks ftag by itself, without release parameter */
static int ospReportUsageFromCookie(struct sip_msg* msg, char* cooky, OSPTCALLID* callid, OSPE_MSG_ROLETYPES type);

/*
 * Create OSP cookie and insert it into Record-Route header
 * param msg SIP message
 * param tansid Transaction ID
 * param uac Source IP
 * param from
 * param to
 * param authtime Request authorization time
 * param isorig Originate / Terminate
 * param destinationCount Destination count
 */
static void ospRecordTransaction(
    struct sip_msg* msg, 
    unsigned long long transid,
    char* uac, 
    char* from, 
    char* to, 
    time_t authtime, 
    int isorig,
    unsigned destinationCount)
{
    const str* name;
    str cookie;
    char buffer[OSP_STRBUF_SIZE];

    LOG(L_DBG, "osp: ospRecordTransaction\n");

    cookie.s = buffer;

    if (isorig == 1) {
        cookie.len = snprintf(
            buffer,
            sizeof(buffer),
            "%c%llu_%c%s_%c%d_%c%d",
            OSP_COOKIE_TRANSID,
            transid,
            OSP_COOKIE_SRCIP,
            uac,
            OSP_COOKIE_AUTHTIME,
            (unsigned int)authtime,
            OSP_COOKIE_DSTCOUNT,
            destinationCount);
    } else {
        cookie.len = snprintf(
            buffer,
            sizeof(buffer),
            "%c%llu_%c%s_%c%d",
            OSP_COOKIE_TRANSID,
            transid,
            OSP_COOKIE_SRCIP,
            uac,
            OSP_COOKIE_AUTHTIME,
            (unsigned int)authtime);
    }

    if (cookie.len < 0) {
        LOG(L_ERR, "osp: ERROR: failed to create OSP cookie\n");
        return;
    }

    /* SER uses AVP to add RR parameters */
    LOG(L_DBG, "osp: adding RR parameter '%s' for '%s'\n", 
        buffer, 
        (isorig == 1) ? "orig" : "term");
    name = (isorig == 1) ? &OSP_ORIGCOOKIE_NAME : &OSP_TERMCOOKIE_NAME;
    add_avp(AVP_NAME_STR | AVP_VAL_STR, (int_str)*name, (int_str)cookie);
}

/*
 * Create OSP originate cookie and insert it into Record-Route header
 * param msg SIP message
 * param tansid Transaction ID
 * param uac Source IP
 * param from
 * param to
 * param authtime Request authorization time
 * param destinationCount Destination count
 */
void ospRecordOrigTransaction(
    struct sip_msg* msg, 
    unsigned long long transid, 
    char* uac, 
    char* from, 
    char* to, 
    time_t authtime,
    unsigned destinationCount)
{
    int isorig = 1;

    LOG(L_DBG, "osp: ospRecordOrigTransaction\n");

    ospRecordTransaction(msg, transid, uac, from, to, authtime, isorig, destinationCount);
}

/*
 * Create OSP terminate cookie and insert it into Record-Route header
 * param msg SIP message
 * param tansid Transaction ID
 * param uac Source IP
 * param from
 * param to
 * param authtime Request authorization time
 */
void ospRecordTermTransaction(
    struct sip_msg* msg, 
    unsigned long long transid, 
    char* uac, 
    char* from, 
    char* to, 
    time_t authtime)
{
    int isorig = 0;
    unsigned destinationCount = 0; /* N/A */

    LOG(L_DBG, "osp: ospRecordTermTransaction\n");

    ospRecordTransaction(msg, transid, uac, from, to, authtime, isorig, destinationCount);
}

/*
 * Report OSP usage from OSP cookie
 *     SER checks ftag by itself, without release parameter
 * param msg SIP message
 * param cookie OSP cookie
 * param callid Call ID
 * param type Usage type
 * return
 */
static int ospReportUsageFromCookie(
    struct sip_msg* msg,
    char* cookie, 
    OSPTCALLID* callid, 
    OSPE_MSG_ROLETYPES type)
{
    int release;
    char* tmp;
    char* token;
    char tag;
    char* value;
    unsigned long long transid = 0;
    time_t authtime = 0;
    unsigned destinationCount = 0;
    time_t endtime = time(NULL);
    char firstvia[OSP_STRBUF_SIZE];
    char from[OSP_STRBUF_SIZE];
    char to[OSP_STRBUF_SIZE];
    char nexthop[OSP_STRBUF_SIZE];
    char* calling;
    char* called;
    char* originator = NULL;
    char* terminator;
    char* source;
    char srcbuf[OSP_STRBUF_SIZE];
    char* destination;
    char dstbuf[OSP_STRBUF_SIZE];
    char* srcdev;
    char devbuf[OSP_STRBUF_SIZE];
    OSPTTRANHANDLE transaction = -1;
    int errorcode;

    LOG(L_DBG, "osp: ospReportUsageFromCookie\n");

    LOG(L_DBG, "osp: '%s' type '%d'\n", cookie, type);
    if (cookie != NULL) {
        for (token = strtok_r(cookie, "_", &tmp);
            token;
            token = strtok_r(NULL, "_", &tmp))
        {
            tag = *token;
            value= token + 1;

            switch (tag) {
                case OSP_COOKIE_TRANSID:
                case OSP_COOKIE_TRANSIDUP:
                    transid = atoll(value);
                    break;
                case OSP_COOKIE_AUTHTIME:
                case OSP_COOKIE_AUTHTIMEUP:
                    authtime = atoi(value);
                    break;
                case OSP_COOKIE_SRCIP:
                case OSP_COOKIE_SRCIPUP:
                    originator = value;
                    break;
                case OSP_COOKIE_DSTCOUNT:
                case OSP_COOKIE_DSTCOUNTUP:
                    destinationCount = (unsigned)atoi(value);
                    break;
                default:
                    LOG(L_ERR, "osp: ERROR: unexpected tag '%c' / value '%s'\n", tag, value);
                    break;
            }
        }
    }

    ospGetSourceAddress(msg, firstvia, sizeof(firstvia));
    ospGetFromUserpart(msg, from, sizeof(from));
    ospGetToUserpart(msg, to, sizeof(to));
    ospGetNextHop(msg, nexthop, sizeof(nexthop));

    LOG(L_DBG, "osp: first via '%s' from '%s' to '%s' next hop '%s'\n",
        firstvia,
        from,
        to,
        nexthop);

    /* SER checks ftag by itself */
    errorcode = ospGetDirection(msg);
    switch (errorcode) {
        case 0:
            release = OSP_RELEASE_ORIG;
            break;
        case 1:
            release = OSP_RELEASE_TERM;
            break;
        default:
            /* This approach has a problem of flipping called/calling number */
            if (strcmp(firstvia, originator) == 0) {
                release = OSP_RELEASE_ORIG;
            } else {
                release = OSP_RELEASE_TERM;
            }
    }

    if (release == OSP_RELEASE_ORIG) {
        LOG(L_DBG,
            "osp: orig '%s' released the call, call_id '%.*s' transaction_id '%llu'\n",
            firstvia,
            callid->ospmCallIdLen,
            callid->ospmCallIdVal,
            transid);
        if (originator == NULL) {
            originator = firstvia;
        }
        calling = from;
        called = to;
        terminator = nexthop;
    } else {
        release = OSP_RELEASE_TERM;
        LOG(L_DBG,
            "osp: term '%s' released the call, call_id '%.*s' transaction_id '%llu'\n",
            firstvia,
            callid->ospmCallIdLen,
            callid->ospmCallIdVal,
            transid);
        if (originator == NULL) {
            originator = nexthop;
        }
        calling = to;
        called = from;
        terminator = firstvia;
    }

    errorcode = OSPPTransactionNew(_osp_provider, &transaction);

    LOG(L_DBG, "osp: created transaction handle '%d' (%d)\n", transaction, errorcode);

    switch (type) {
        case OSPC_DESTINATION:
            ospConvertAddress(originator, srcbuf, sizeof(srcbuf));
            source = srcbuf;
            destination = _osp_device_ip;
            srcdev = "";
            break;
        case OSPC_SOURCE:
        case OSPC_OTHER:
        case OSPC_UNDEFINED_ROLE:
        default:
            source = _osp_device_ip;
            ospConvertAddress(terminator, dstbuf, sizeof(dstbuf));
            destination = dstbuf;
            ospConvertAddress(originator, devbuf, sizeof(devbuf));
            srcdev = devbuf;
            break;
    }

    errorcode = OSPPTransactionBuildUsageFromScratch(
        transaction,
        transid,
        type,
        source,
        destination,
        srcdev,
        "",
        calling,
        OSPC_E164,
        called,
        OSPC_E164,
        callid->ospmCallIdLen,
        callid->ospmCallIdVal,
        (enum OSPEFAILREASON)0,
        NULL,
        NULL);

    LOG(L_DBG, "osp: built usage handle '%d' (%d)\n", transaction, errorcode);

    if ((errorcode == OSPC_ERR_NO_ERROR) && (destinationCount > 0)) {
        errorcode = OSPPTransactionSetDestinationCount(
            transaction,
            destinationCount);
    }

    ospReportUsageWrapper(
        transaction,
        cookie == NULL ? 9016 : 10016,
        cookie == NULL ? 0 : endtime - authtime,
        authtime,
        endtime,
        0,0,
        0,0,
        release);

    return errorcode;
}

/*
 * Report OSP usage
 *     SER uses AVP to add RR pararmters
 * param msg SIP message
 * param ignore1
 * param ignore2
 * return MODULE_RETURNCODE_TRUE success, MODULE_RETURNCODE_FALSE failure
 */
int ospReportUsage(
    struct sip_msg* msg, 
    char* ignore1,
    char* ignore2)
{
    int_str cookieval;
    struct usr_avp* cookieavp = NULL;
    static const int FROMFLAGS = AVP_TRACK_FROM | AVP_CLASS_URI | AVP_NAME_STR | AVP_VAL_STR;
    static const int TOFLAGS = AVP_TRACK_TO | AVP_CLASS_URI | AVP_NAME_STR | AVP_VAL_STR;
    char buffer[OSP_HEADERBUF_SIZE];
    OSPTCALLID* callid = NULL;
    int result = MODULE_RETURNCODE_FALSE;

    LOG(L_DBG, "osp: ospReportUsage\n");

    ospGetCallId(msg, &callid);

    if (callid != NULL) {
        if ((cookieavp = search_first_avp(FROMFLAGS, (int_str)OSP_ORIGCOOKIE_NAME, &cookieval, NULL)) != 0 ||
            (cookieavp = search_first_avp(TOFLAGS, (int_str)OSP_ORIGCOOKIE_NAME, &cookieval, NULL)) != 0)
        {
            ospCopyStrToBuffer(&cookieval.s, buffer, sizeof(buffer));
            LOG(L_DBG, "orig cookie '%s'\n", buffer);
            LOG(L_INFO,
                "osp: report orig duration for call_id '%.*s'\n",
                callid->ospmCallIdLen,
                callid->ospmCallIdVal);
            ospReportUsageFromCookie(msg, buffer, callid, OSPC_SOURCE);
            result = MODULE_RETURNCODE_TRUE;
        }

        if ((cookieavp = search_first_avp(FROMFLAGS, (int_str)OSP_TERMCOOKIE_NAME, &cookieval, NULL)) != 0 ||
            (cookieavp = search_first_avp(TOFLAGS, (int_str)OSP_TERMCOOKIE_NAME, &cookieval, NULL)) != 0)
        {
            ospCopyStrToBuffer(&cookieval.s, buffer, sizeof(buffer));
            LOG(L_DBG, "term cookie '%s'\n", buffer);
            LOG(L_INFO,
                "osp: report term duration for call_id '%.*s'\n",
                callid->ospmCallIdLen,
                callid->ospmCallIdVal);
            ospReportUsageFromCookie(msg, buffer, callid, OSPC_DESTINATION);
            result = MODULE_RETURNCODE_TRUE;
        }

        if (result == MODULE_RETURNCODE_FALSE) {
            LOG(L_DBG, "without orig or term OSP information\n");
            LOG(L_INFO, "report other duration for call_id '%.*s'\n",
               callid->ospmCallIdLen,
               callid->ospmCallIdVal);
            ospReportUsageFromCookie(msg, NULL, callid, OSPC_SOURCE);
            result = MODULE_RETURNCODE_TRUE;
        }

        OSPPCallIdDelete(&callid);
    }

    if (result == MODULE_RETURNCODE_FALSE) {
        LOG(L_DBG, "osp: failed to report usage\n");
    }

    return result;
}

/*
 * Build OSP usage from destination
 * param transaction OSP transaction handle
 * param dest Destination
 * param lastcode Destination status
 * return 0 success, others failure
 */
static int ospBuildUsageFromDestination(
    OSPTTRANHANDLE transaction, 
    osp_dest* dest, 
    int lastcode)
{
    int errorcode;
    char addr[OSP_STRBUF_SIZE];
    char* source;
    char* srcdev;

    LOG(L_DBG, "osp: ospBuildUsageFromDestination\n");

    if (dest->type == OSPC_SOURCE) {
        ospConvertAddress(dest->srcdev, addr, sizeof(addr));
        source = dest->source;
        srcdev = addr;
    } else {
        ospConvertAddress(dest->source, addr, sizeof(addr));
        source = addr;
        srcdev = dest->srcdev;
    }

    errorcode = OSPPTransactionBuildUsageFromScratch(
        transaction,
        dest->transid,
        dest->type,
        source,
        dest->host,
        srcdev,
        dest->destdev,
        dest->calling,
        OSPC_E164,
        dest->origcalled,       /* Report original called number */
        OSPC_E164,
        dest->callidsize,
        dest->callid,
        (enum OSPEFAILREASON)lastcode,
        NULL,
        NULL);

    return errorcode;
}

/*
 * Report OSP usage from destination
 * param transaction OSP transaction handle
 * param dest Destination
 * return 0 success
 */
static int ospReportUsageFromDestination(
    OSPTTRANHANDLE transaction, 
    osp_dest* dest)
{
    ospReportUsageWrapper(
        transaction,                                          /* In - Transaction handle */
        dest->lastcode,                                       /* In - Release Code */    
        0,                                                    /* In - Length of call */
        dest->authtime,                                       /* In - Call start time */
        0,                                                    /* In - Call end time */
        dest->time180,                                        /* In - Call alert time */
        dest->time200,                                        /* In - Call connect time */
        dest->time180 ? 1 : 0,                                /* In - Is PDD Info present */
        dest->time180 ? dest->time180 - dest->authtime : 0,   /* In - Post Dial Delay */
        0);

    return 0;
}

/*
 * Report originate call setup usage
 */
void ospReportOrigSetupUsage(void)
{
    osp_dest* dest = NULL;
    osp_dest* lastused = NULL;
    struct usr_avp* destavp = NULL;
    int_str destval;
    struct search_state state;
    OSPTTRANHANDLE transaction = -1;
    int lastcode = 0;
    int errorcode;

    LOG(L_DBG, "osp: ospReportOrigSetupUsage\n");

    errorcode = OSPPTransactionNew(_osp_provider, &transaction);

    for (destavp = search_first_avp(AVP_NAME_STR | AVP_VAL_STR, (int_str)OSP_ORIGDEST_NAME, &destval, &state);
        destavp != NULL;
        destavp = search_next_avp(&state, &destval)) 
    {
        /* OSP destination is wrapped in a string */
        dest = (osp_dest*)destval.s.s;

        if (dest->used == 1) {
            if (dest->reported == 1) {
                LOG(L_DBG, "osp: orig setup already reported\n");
                break;
            } else {
                dest->reported = 1;
            }

            LOG(L_DBG, "osp: iterating through used destination\n");

            ospDumpDestination(dest);

            lastused = dest;

            errorcode = ospBuildUsageFromDestination(transaction, dest, lastcode);

            lastcode = dest->lastcode;
        } else {
            LOG(L_DBG, "osp: destination has not been used, breaking out\n");
            break;
        }
    }

    if (lastused) {
        LOG(L_INFO,
            "osp: report orig setup for call_id '%.*s' transaction_id '%llu'\n",
            lastused->callidsize,
            lastused->callid,
            lastused->transid);
        errorcode = ospReportUsageFromDestination(transaction, lastused);
    } else {
        /* If a Toolkit transaction handle was created, but we did not find
         * any destinations to report, we need to release the handle. Otherwise,
         * the ospReportUsageFromDestination will release it.
         */
        OSPPTransactionDelete(transaction);
    }
}

/*
 * Report terminate call setup usage
 */
void ospReportTermSetupUsage(void)
{
    osp_dest* dest = NULL;
    OSPTTRANHANDLE transaction = -1;
    int errorcode;

    LOG(L_DBG, "osp: ospReportTermSetupUsage\n");

    if ((dest = ospGetTermDestination())) {
        if (dest->reported == 0) {
            dest->reported = 1;
            LOG(L_INFO,
                "osp: report term setup for call_id '%.*s' transaction_id '%llu'\n",
                dest->callidsize,
                dest->callid,
                dest->transid);
            errorcode = OSPPTransactionNew(_osp_provider, &transaction);
            errorcode = ospBuildUsageFromDestination(transaction, dest, 0);
            errorcode = ospReportUsageFromDestination(transaction, dest);
        } else {
            LOG(L_DBG, "osp: term setup already reported\n");
        }
    } else {
        LOG(L_ERR, "osp: ERROR: without term setup to report\n");
    }
}
