/* GPRS SGSN CDR dumper */

/* (C) 2015 by Holger Hans Peter Freyther
 * All Rights Reserved
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program 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 Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

#include <osmocom/ctrl/control_if.h>

#include <osmocom/gtp/gtp.h>
#include <osmocom/gtp/pdp.h>

#include <osmocom/sgsn/sgsn.h>
#include <osmocom/sgsn/signal.h>
#include <osmocom/sgsn/gprs_utils.h>
#include <osmocom/sgsn/debug.h>
#include <osmocom/gsm/apn.h>

#include <osmocom/sgsn/vty.h>
#include <osmocom/sgsn/gtp_ggsn.h>
#include <osmocom/sgsn/pdpctx.h>
#include <osmocom/sgsn/mmctx.h>

#include <arpa/inet.h>

#include <time.h>

#include <stdio.h>
#include <inttypes.h>

/**
 * The CDR module will generate an entry like:
 *
 * IMSI, # Subscriber IMSI
 * IMEI, # Subscriber IMEI
 * MSISDN, # Subscriber MISDN
 * Charging_Timestamp, # Event start Time
 * Charging_UTC, # Time zone of event start time
 * Duration, # Session DURATION
 * Cell_Id, # CELL_ID
 * Location_Area, # LAC
 * GGSN_ADDR, # GGSN_ADDR
 * SGSN_ADDR, # SGSN_ADDR
 * APNI, # APNI
 * PDP_ADDR, # PDP_ADDR
 * VOL_IN, # VOL_IN in Bytes
 * VOL_OUT, # VOL_OUT in Bytes
 * CAUSE_FOR_TERM, # CAUSE_FOR_TERM
 */

static void send_cdr_trap(char *value)
{
	if (ctrl_cmd_send_trap(sgsn->ctrlh, "cdr-v1", value) < 0)
		LOGP(DGPRS, LOGL_ERROR, "Failed to create and send TRAP cdr-v1\n");
}

static void maybe_print_header(FILE *cdr_file)
{
	if (ftell(cdr_file) != 0)
		return;

	fprintf(cdr_file, "timestamp,imsi,imei,msisdn,cell_id,lac,hlr,event,pdp_duration,ggsn_addr,sgsn_addr,apni,eua_addr,vol_in,vol_out,charging_id\n");
}

static int cdr_snprintf_mm(char *buf, size_t size, const char *ev,
			struct sgsn_mm_ctx *mmctx)
{
	struct tm tm;
	struct timeval tv;
	int ret;

	gettimeofday(&tv, NULL);
	gmtime_r(&tv.tv_sec, &tm);
	ret = snprintf(buf, size, "%04d%02d%02d%02d%02d%02d%03d,%s,%s,%s,%d,%d,%s,%s",
		tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday,
		tm.tm_hour, tm.tm_min, tm.tm_sec,
		(int)(tv.tv_usec / 1000),
		mmctx->imsi,
		mmctx->imei,
		mmctx->msisdn,
		mmctx->gb.cell_id,
		mmctx->ra.lac.lac,
		mmctx->hlr,
		ev);
	return ret;
}

static void cdr_log_mm(struct sgsn_instance *inst, const char *ev,
			struct sgsn_mm_ctx *mmctx)
{
	FILE *cdr_file;
	char buf[1024];

	if (!inst->cfg.cdr.filename && !inst->cfg.cdr.trap)
		return;

	cdr_snprintf_mm(buf, sizeof(buf), ev, mmctx);

	if (inst->cfg.cdr.trap)
		send_cdr_trap(buf);

	if (inst->cfg.cdr.filename) {
		cdr_file = fopen(inst->cfg.cdr.filename, "a");
		if (!cdr_file) {
			LOGP(DGPRS, LOGL_ERROR, "Failed to open %s\n",
				inst->cfg.cdr.filename);
			return;
		}

		maybe_print_header(cdr_file);
		fprintf(cdr_file, "%s\n", buf);

		fclose(cdr_file);
	}
}

static void extract_eua(struct ul66_t *eua, char *eua_addr)
{
	if (eua->l < 2)
		return;

	/* there is no addr for ETSI/PPP */
	if ((eua->v[0] & 0x0F) != 1) {
		strcpy(eua_addr, "ETSI");
		return;
	}

	if (eua->v[1] == 0x21 && eua->l == 6)
		inet_ntop(AF_INET, &eua->v[2], eua_addr, INET_ADDRSTRLEN);
	else if (eua->v[1] == 0x57 && eua->l == 18)
		inet_ntop(AF_INET6, &eua->v[2], eua_addr, INET6_ADDRSTRLEN);
	else {
		/* e.g. both IPv4 and IPv6 */
		strcpy(eua_addr, "Unknown address");
	}
}

static int cdr_snprintf_pdp(char *buf, size_t size, const char *ev,
			struct sgsn_pdp_ctx *pdp)
{
	char apni[(pdp->lib ? pdp->lib->apn_use.l : 0) + 1];
	char ggsn_addr[INET_ADDRSTRLEN];
	char sgsn_addr[INET_ADDRSTRLEN];
	char eua_addr[INET6_ADDRSTRLEN];
	struct tm tm;
	struct timeval tv;
	time_t duration;
	struct timespec tp;
	int ret;

	memset(apni, 0, sizeof(apni));
	memset(ggsn_addr, 0, sizeof(ggsn_addr));
	memset(sgsn_addr, 0, sizeof(sgsn_addr));
	memset(eua_addr, 0, sizeof(eua_addr));


	if (pdp->lib) {
		osmo_apn_to_str(apni, pdp->lib->apn_use.v, pdp->lib->apn_use.l);
		inet_ntop(AF_INET, &pdp->lib->hisaddr0.s_addr, ggsn_addr, sizeof(ggsn_addr));
		extract_eua(&pdp->lib->eua, eua_addr);
	}

	if (pdp->ggsn)
		inet_ntop(AF_INET, &pdp->ggsn->gsn->gsnc.s_addr, sgsn_addr, sizeof(sgsn_addr));

	osmo_clock_gettime(CLOCK_MONOTONIC, &tp);
	gettimeofday(&tv, NULL);

	/* convert the timestamp to UTC */
	gmtime_r(&tv.tv_sec, &tm);

	/* Check the duration of the PDP context */
	duration = tp.tv_sec - pdp->cdr_start.tv_sec;

	ret = snprintf(buf, size,
		"%04d%02d%02d%02d%02d%02d%03d,%s,%s,%s,%d,%d,%s,%s,%ld,%s,%s,%s,%s,%" PRIu64 ",%" PRIu64 ",%u",
		tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday,
		tm.tm_hour, tm.tm_min, tm.tm_sec,
		(int)(tv.tv_usec / 1000),
		pdp->mm ? pdp->mm->imsi : "N/A",
		pdp->mm ? pdp->mm->imei : "N/A",
		pdp->mm ? pdp->mm->msisdn : "N/A",
		pdp->mm ? pdp->mm->gb.cell_id : -1,
		pdp->mm ? pdp->mm->ra.lac.lac : -1,
		pdp->mm ? pdp->mm->hlr : "N/A",
		ev,
		(unsigned long ) duration,
		ggsn_addr,
		sgsn_addr,
		apni,
		eua_addr,
		pdp->cdr_bytes_in,
		pdp->cdr_bytes_out,
		pdp->cdr_charging_id);
	return ret;
}

static void cdr_log_pdp(struct sgsn_instance *inst, const char *ev,
			struct sgsn_pdp_ctx *pdp)
{
	FILE *cdr_file;
	char buf[1024];

	if (!inst->cfg.cdr.filename && !inst->cfg.cdr.trap)
		return;

	cdr_snprintf_pdp(buf, sizeof(buf), ev, pdp);

	if (inst->cfg.cdr.trap)
		send_cdr_trap(buf);

	if (inst->cfg.cdr.filename) {
		cdr_file = fopen(inst->cfg.cdr.filename, "a");
		if (!cdr_file) {
			LOGP(DGPRS, LOGL_ERROR, "Failed to open %s\n",
				inst->cfg.cdr.filename);
			return;
		}

		maybe_print_header(cdr_file);
		fprintf(cdr_file, "%s\n", buf);
		fclose(cdr_file);
	}
}

static void cdr_pdp_timeout(void *_data)
{
	struct sgsn_pdp_ctx *pdp = _data;
	cdr_log_pdp(sgsn, "pdp-periodic", pdp);
	osmo_timer_schedule(&pdp->cdr_timer, sgsn->cfg.cdr.interval, 0);
}

static int handle_sgsn_sig(unsigned int subsys, unsigned int signal,
			void *handler_data, void *_signal_data)
{
	struct sgsn_signal_data *signal_data = _signal_data;
	struct sgsn_instance *inst = handler_data;

	if (subsys != SS_SGSN)
		return 0;

	switch (signal) {
	case S_SGSN_ATTACH:
		cdr_log_mm(inst, "attach", signal_data->mm);
		break;
	case S_SGSN_UPDATE:
		cdr_log_mm(inst, "update", signal_data->mm);
		break;
	case S_SGSN_DETACH:
		cdr_log_mm(inst, "detach", signal_data->mm);
		break;
	case S_SGSN_MM_FREE:
		cdr_log_mm(inst, "free", signal_data->mm);
		break;
	case S_SGSN_PDP_ACT:
		osmo_clock_gettime(CLOCK_MONOTONIC, &signal_data->pdp->cdr_start);
		signal_data->pdp->cdr_charging_id = signal_data->pdp->lib->cid;
		cdr_log_pdp(inst, "pdp-act", signal_data->pdp);
		osmo_timer_setup(&signal_data->pdp->cdr_timer, cdr_pdp_timeout,
				 signal_data->pdp);
		osmo_timer_schedule(&signal_data->pdp->cdr_timer, inst->cfg.cdr.interval, 0);
		break;
	case S_SGSN_PDP_DEACT:
		cdr_log_pdp(inst, "pdp-deact", signal_data->pdp);
		osmo_timer_del(&signal_data->pdp->cdr_timer);
		break;
	case S_SGSN_PDP_TERMINATE:
		cdr_log_pdp(inst, "pdp-terminate", signal_data->pdp);
		osmo_timer_del(&signal_data->pdp->cdr_timer);
		break;
	case S_SGSN_PDP_FREE:
		cdr_log_pdp(inst, "pdp-free", signal_data->pdp);
		osmo_timer_del(&signal_data->pdp->cdr_timer);
		break;
	}

	return 0;
}

int sgsn_cdr_init(struct sgsn_instance *sgsn)
{
	/* register for CDR related events */
	sgsn->cfg.cdr.interval = 10 * 60;
	osmo_signal_register_handler(SS_SGSN, handle_sgsn_sig, sgsn);

	return 0;
}

void sgsn_cdr_release(struct sgsn_instance *sgsn)
{
	osmo_signal_unregister_handler(SS_SGSN, handle_sgsn_sig, sgsn);
}
