/* JSON helpers for OsmoBTS: stats, rate-counters, bts and transceiver
 * serialization API implementation
 *
 * Copyright (C) 2026 Timur Davydov <dtv.comp@gmail.com>
 *
 * 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 <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <inttypes.h>
#include <stdbool.h>
#include <stdarg.h>

#include "stats_json.h"

#include <osmo-bts/bts.h>
#include <osmo-bts/abis.h>
#include <osmo-bts/logging.h>
#include <osmo-bts/scheduler.h>
#include "l1_if.h"
#include "trx_if.h"

#include <osmocom/core/counter.h>
#include <osmocom/core/rate_ctr.h>
#include <osmocom/core/stat_item.h>

/* g_bts is defined elsewhere */
extern struct gsm_bts *g_bts;

extern uint64_t sdr_send_ts;
extern uint64_t g_last_ts;
extern unsigned wptr;
extern unsigned rptr;
extern unsigned total_tx_late;
extern unsigned start_fn;

/* JSON stats context */
struct stats_json_ctx {
	char *buf;
	size_t buflen;
	size_t off;
	bool first;
	bool first_group;
	bool first_ctr;
	bool first_item;
	const char *cur_group_desc;
};

/* append a single character into ctx->buf */
static int sj_append_char(struct stats_json_ctx *ctx, char c)
{
	if (ctx->off + 1 >= ctx->buflen)
		return -ENOSPC;

	ctx->buf[ctx->off++] = c;
	ctx->buf[ctx->off] = '\0';

	return 0;
}

/* append a formatted string into ctx->buf */
static int sj_append_fmt(struct stats_json_ctx *ctx, const char *fmt, ...)
{
	va_list ap;
	int rc;
	size_t rem;

	if (ctx->off >= ctx->buflen)
		return -ENOSPC;

	rem = ctx->buflen - ctx->off;
	va_start(ap, fmt);
	rc = vsnprintf(ctx->buf + ctx->off, rem, fmt, ap);
	va_end(ap);

	if (rc < 0)
		return -EIO;
	if ((size_t)rc >= rem)
		return -ENOSPC;

	ctx->off += rc;
	return 0;
}

/* simple JSON string escaper that appends into ctx->buf */
static int sj_append_json_str(struct stats_json_ctx *ctx, const char *s)
{
	if (!s) s = "";

	if (sj_append_char(ctx, '"'))
		return -ENOSPC;
	for (size_t j = 0; s[j] != '\0'; j++) {
		char c = s[j];
		if (c == '"' || c == '\\') {
			int rc = sj_append_char(ctx, '\\');
			if (rc)
				return rc;
			rc = sj_append_char(ctx, c);
			if (rc)
				return rc;
		} else if ((unsigned char)c < 0x20) {
			int rc = sj_append_char(ctx, ' ');
			if (rc)
				return rc;
		} else {
			int rc = sj_append_char(ctx, c);
			if (rc)
				return rc;
		}
	}

	return sj_append_char(ctx, '"');
}

/* handle a single counter */
static int sj_counters_handler(struct osmo_counter *counter, void *ctxv)
{
	struct stats_json_ctx *ctx = ctxv;
	unsigned long val = counter->value;
	int rc;

	if (!ctx->first) {
		rc = sj_append_char(ctx, ',');
		if (rc)
			return rc;
	}
	ctx->first = false;

	rc = sj_append_fmt(ctx, "{\"name\":");
	if (rc)
		return rc;

	rc = sj_append_json_str(ctx, counter->name);
	if (rc)
		return rc;

	return sj_append_fmt(ctx, ",\"value\":%lu}", val);
}

/* Rate counters: produce entries with name + current + per_s
 * per second doesn't work at the moment because osmo_fd_timer is not implemented
 */
static int sj_rate_ctr_counter_handler(struct rate_ctr_group *g, struct rate_ctr *ctr,
				       const struct rate_ctr_desc *desc, void *ctxv)
{
	struct stats_json_ctx *ctx = ctxv;
	int rc;

	(void)g;

	if (!ctx->first_ctr) {
		rc = sj_append_char(ctx, ',');
		if (rc)
			return rc;
	}
	ctx->first_ctr = false;

	rc = sj_append_fmt(ctx, "{\"name\":");
	if (rc)
		return rc;

	rc = sj_append_json_str(ctx, desc->name);
	if (rc)
		return rc;

	return sj_append_fmt(ctx, ",\"current\":%" PRIu64 "}", ctr->current);
#if 0
	/* Per second rate doesn't work at the moment because osmo_fd_timer is not implemented */
	return sj_append_fmt(ctx, ",\"current\":%" PRIu64 ",\"per_s\":%" PRIu64 "}",
			     ctr->current, ctr->intv[RATE_CTR_INTV_SEC].rate);
#endif
}

/* Rate counter groups: produce entries with group + counters */
static int sj_rate_ctr_group_handler(struct rate_ctr_group *ctrg, void *ctxv)
{
	struct stats_json_ctx *ctx = ctxv;
	int rc;

	if (!ctx->first_group) {
		rc = sj_append_char(ctx, ',');
		if (rc)
			return rc;
	}
	ctx->first_group = false;

	rc = sj_append_fmt(ctx, "{\"group_description\":");
	if (rc)
		return rc;

	rc = sj_append_json_str(ctx, ctrg->desc->group_description);
	if (rc)
		return rc;

	rc = sj_append_fmt(ctx, ",\"counters\":[");
	if (rc)
		return rc;

	ctx->first_ctr = true;
	rc = rate_ctr_for_each_counter(ctrg, sj_rate_ctr_counter_handler, ctx);
	if (rc)
		return rc;

	return sj_append_fmt(ctx, "]}");
}

/* Flat rate-counters: produce entries with group + name + current + per_s
 * per second doesn't work at the moment because osmo_fd_timer is not implemented
 */
static int sj_rate_ctr_flat_counter_handler(struct rate_ctr_group *g, struct rate_ctr *ctr,
					    const struct rate_ctr_desc *desc, void *ctxv)
{
	struct stats_json_ctx *ctx = ctxv;
	int rc;

	(void)g;

	if (!ctx->first_ctr) {
		rc = sj_append_char(ctx, ',');
		if (rc)
			return rc;
	}
	ctx->first_ctr = false;

	rc = sj_append_fmt(ctx, "{\"group\":");
	if (rc)
		return rc;

	rc = sj_append_json_str(ctx, ctx->cur_group_desc);
	if (rc)
		return rc;

	rc = sj_append_fmt(ctx, ",\"name\":");
	if (rc)
		return rc;

	rc = sj_append_json_str(ctx, desc->name);
	if (rc)
		return rc;

	return sj_append_fmt(ctx, ",\"current\":%" PRIu64 "}", ctr->current);
#if 0
	/* Per second rate doesn't work at the moment because osmo_fd_timer is not implemented */
	return sj_append_fmt(ctx, ",\"current\":%" PRIu64 ",\"per_s\":%" PRIu64 "}",
			     ctr->current, ctr->intv[RATE_CTR_INTV_SEC].rate);
#endif
}

/* Rate counter groups: produce entries with group + counters */
static int sj_rate_ctr_group_flat_handler(struct rate_ctr_group *ctrg, void *ctxv)
{
	struct stats_json_ctx *ctx = ctxv;
	int rc;

	ctx->cur_group_desc = ctrg->desc->group_description ? ctrg->desc->group_description : NULL;
	rc = rate_ctr_for_each_counter(ctrg, sj_rate_ctr_flat_counter_handler, ctx);

	ctx->cur_group_desc = NULL;

	return rc;
}

/* TRX / Transceiver timeslot JSON */
static int sj_transceiver_timeslot(struct gsm_bts_trx *trx, unsigned int tn, struct stats_json_ctx *ctx)
{
	const struct gsm_bts_trx_ts *ts = &trx->ts[tn];
	const struct l1sched_ts *l1ts = ts->priv;
	int rc;

	if (!l1ts)
		return 0;

	if (!ctx->first_item) {
		rc = sj_append_char(ctx, ',');
		if (rc)
			return rc;
	}
	ctx->first_item = false;

	rc = sj_append_fmt(ctx, "{\"tn\":%u,\"mf\":", tn);
	if (rc)
		return rc;

	rc = sj_append_json_str(ctx, (const char *)trx_sched_multiframes[l1ts->mf_index].name);
	if (rc)
		return rc;

	return sj_append_fmt(ctx, ",\"pending_dl_prims\":%u,\"interference\":%d,\"unit\":\"dBm\"}",
			     (unsigned)llist_count(&l1ts->dl_prims),
			     l1ts->chan_state[TRXC_IDLE].meas.interf_avg);
}

/* TRX / Transceiver JSON */
static int sj_transceiver_handler_ll(struct stats_json_ctx *ctx)
{
	struct gsm_bts_trx *trx;
	bool first_trx = true;
	int rc;

	if (!g_bts)
		return 0;

	llist_for_each_entry(trx, &g_bts->trx_list, list) {
		struct phy_instance *pinst = trx_phy_instance(trx);
		struct phy_link *plink = pinst->phy_link;
		struct trx_l1h *l1h = pinst->u.osmotrx.hdl;

		if (!first_trx) {
			rc = sj_append_char(ctx, ',');
			if (rc)
				return rc;
		}
		first_trx = false;

		rc = sj_append_fmt(ctx, "{\"nr\":%u,\"source\":", trx->nr);
		if (rc)
			return rc;

		const char *sname = plink->u.osmotrx.trx_clk_iofd
			? osmo_iofd_get_name(plink->u.osmotrx.trx_clk_iofd) : "";
		rc = sj_append_json_str(ctx, sname);
		if (rc)
			return rc;

		rc = sj_append_fmt(ctx, ",\"poweron\":%s,\"phy_link_state\":",
				   trx_if_powered(l1h) ? "true" : "false");
		if (rc)
			return rc;

		rc = sj_append_json_str(ctx, phy_link_state_name(phy_link_state_get(plink)));
		if (rc)
			return rc;

		/* arfcn/tsc/bsic */
		if (l1h->config.arfcn_valid)
			rc = sj_append_fmt(ctx, ",\"arfcn\":%d", l1h->config.arfcn & ~ARFCN_PCS);
		else
			rc = sj_append_fmt(ctx, ",\"arfcn\":null");

		if (rc)
			return rc;

		if (l1h->config.tsc_valid)
			rc = sj_append_fmt(ctx, ",\"tsc\":%d", l1h->config.tsc);
		else
			rc = sj_append_fmt(ctx, ",\"tsc\":null");

		if (rc)
			return rc;

		if (l1h->config.bsic_valid)
			rc = sj_append_fmt(ctx, ",\"bsic\":%d", l1h->config.bsic);
		else
			rc = sj_append_fmt(ctx, ",\"bsic\":null");

		if (rc)
			return rc;

		/* timeslots */
		rc = sj_append_fmt(ctx, ",\"timeslots\":[");
		if (rc)
			return rc;

		ctx->first_item = true;
		for (unsigned int tn = 0; tn < ARRAY_SIZE(trx->ts); tn++) {
			rc = sj_transceiver_timeslot(trx, tn, ctx);
			if (rc)
				return rc;
		}

		rc = sj_append_fmt(ctx, "]}");
		if (rc)
			return rc;
	}

	return 0;
}

/* Stat items: produce entries with name + value + unit */
static int sj_stat_item_handler(struct osmo_stat_item_group *g, struct osmo_stat_item *item, void *ctxv)
{
	struct stats_json_ctx *ctx = ctxv;
	const struct osmo_stat_item_desc *desc = osmo_stat_item_get_desc(item);
	int32_t value = osmo_stat_item_get_last(item);
	int rc;

	(void)g;

	if (!ctx->first_item) {
		rc = sj_append_char(ctx, ',');
		if (rc)
			return rc;
	}
	ctx->first_item = false;

	rc = sj_append_fmt(ctx, "{\"name\":");
	if (rc)
		return rc;

	rc = sj_append_json_str(ctx, desc->name);
	if (rc)
		return rc;

	rc = sj_append_fmt(ctx, ",\"value\":%d,\"unit\":", value);
	if (rc)
		return rc;

	rc = sj_append_json_str(ctx, desc->unit && desc->unit != OSMO_STAT_ITEM_NO_UNIT ? desc->unit : "");
	if (rc)
		return rc;

	return sj_append_char(ctx, '}');
}

/* Stat item groups: produce entries with group + items */
static int sj_stat_item_group_handler(struct osmo_stat_item_group *statg, void *ctxv)
{
	struct stats_json_ctx *ctx = ctxv;
	int rc;

	if (!ctx->first_group) {
		rc = sj_append_char(ctx, ',');
		if (rc)
			return rc;
	}
	ctx->first_group = false;

	rc = sj_append_fmt(ctx, "{\"group_description\":");
	if (rc)
		return rc;

	rc = sj_append_json_str(ctx, statg->desc->group_description);
	if (rc)
		return rc;

	rc = sj_append_fmt(ctx, ",\"items\":[");
	if (rc)
		return rc;

	ctx->first_item = true;
	rc = osmo_stat_item_for_each_item(statg, sj_stat_item_handler, ctx);
	if (rc)
		return rc;

	return sj_append_fmt(ctx, "]}");
}

/* Build JSON for 'show bts' by enumerating fields of g_bts and related structures */
int bts_to_json(char *buf, size_t buflen)
{
	struct stats_json_ctx ctx = {
		.buf = buf,
		.buflen = buflen,
	};
	int rc;

	if (!buf || buflen == 0)
		return -EINVAL;

	if (g_bts) {
		rc = sj_append_fmt(&ctx,
			"{\"bts_nr\":%u,\"variant\":\"%s\",\"band\":\"%s\",\"cell_identity\":%u,\"lac\":%u"
			",\"bsic\":%u,\"num_trx\":%u,\"description\":",
			g_bts->nr,
			btsvariant2str(g_bts->variant),
			gsm_band_name(g_bts->band),
			g_bts->cell_identity,
			g_bts->location_area_code,
			g_bts->bsic,
			g_bts->num_trx);
		if (rc)
			return rc;

		rc = sj_append_json_str(&ctx, g_bts->description);
		if (rc)
			return rc;

		rc = sj_append_fmt(&ctx,
			",\"unit_site_id\":%u,\"unit_bts_id\":%u,\"oml_connected\":%s,\"pcu_version\":",
			g_bts->ip_access.site_id,
			g_bts->ip_access.bts_id,
			g_bts->oml_link ? "true" : "false");
		if (rc)
			return rc;

		rc = sj_append_json_str(&ctx, g_bts->pcu_version);
		if (rc)
			return rc;

		rc = sj_append_fmt(&ctx,
			",\"paging_queue_max\":%u,\"paging_queue_len\":%u,"
			"\"agch_queue_max\":%u,\"agch_queue_len\":%u,\"agch_dropped\":%llu,\"agch_merged\":%llu"
			",\"agch_rejected\":%llu,\"agch_agch_msgs\":%llu,\"agch_pch_msgs\":%llu,\"smscb_tgt\":%d"
			",\"smscb_max\":%d,\"smscb_hyst\":%d,\"smscb_basic_len\":%u,\"smscb_ext_len\":%u,"
			"\"ph_rts_fn_avg\":%d,\"ph_rts_fn_min\":%d,\"ph_rts_fn_max\":%d"
			",\"radio_link_timeout_current\":%d,\"radio_link_timeout_oml\":%d,\"c0_power_red_db\":%d}",
			paging_get_queue_max(g_bts->paging_state),
			paging_queue_length(g_bts->paging_state),
			g_bts->agch_queue.max_length,
			g_bts->agch_queue.length,
			(unsigned long long)g_bts->agch_queue.dropped_msgs,
			(unsigned long long)g_bts->agch_queue.merged_msgs,
			(unsigned long long)g_bts->agch_queue.rejected_msgs,
			(unsigned long long)g_bts->agch_queue.agch_msgs,
			(unsigned long long)g_bts->agch_queue.pch_msgs,
			g_bts->smscb_queue_tgt_len,
			g_bts->smscb_queue_max_len,
			g_bts->smscb_queue_hyst,
			g_bts->smscb_basic.queue_len,
			g_bts->smscb_extended.queue_len,
			bts_get_avg_fn_advance(g_bts),
			g_bts->fn_stats.min,
			g_bts->fn_stats.max,
			g_bts->radio_link_timeout.current,
			g_bts->radio_link_timeout.oml,
			g_bts->c0_power_red_db);
		if (rc)
			return rc;
	}

	return (int)ctx.off;
}

/* Build JSON for 'show stats' by enumerating counters, rate_ctr groups and stat items */
int stats_to_json(char *buf, size_t buflen)
{
	struct stats_json_ctx ctx;
	int rc;

	if (!buf || buflen == 0)
		return -EINVAL;

	memset(&ctx, 0, sizeof(ctx));
	ctx.buf = buf;
	ctx.buflen = buflen;
	ctx.first = true;
	ctx.first_group = true;
	ctx.first_ctr = true;
	ctx.first_item = true;

	rc = sj_append_char(&ctx, '{');
	if (rc)
		return rc;

	/* Ungrouped counters */
	rc = sj_append_fmt(&ctx, "\"ungrouped_counters\":[");
	if (rc)
		return rc;

	rc = osmo_counters_for_each(sj_counters_handler, &ctx);
	if (rc)
		return rc;

	rc = sj_append_char(&ctx, ']');
	if (rc)
		return rc;

	/* Rate counter groups */
	rc = sj_append_fmt(&ctx, ",\"rate_ctr_groups\":[");
	if (rc)
		return rc;

	ctx.first_group = true;
	rc = rate_ctr_for_each_group(sj_rate_ctr_group_handler, &ctx);
	if (rc)
		return rc;

	rc = sj_append_char(&ctx, ']');
	if (rc)
		return rc;

	/* Stat item groups */
	rc = sj_append_fmt(&ctx, ",\"stat_item_groups\":[");
	if (rc)
		return rc;

	ctx.first_group = true;
	rc = osmo_stat_item_for_each_group(sj_stat_item_group_handler, &ctx);
	if (rc)
		return rc;

	rc = sj_append_char(&ctx, ']');
	if (rc)
		return rc;

	rc = sj_append_char(&ctx, '}');
	if (rc)
		return rc;

	return (int)ctx.off;
}

/* Build JSON for 'show rate-counters' by enumerating rate counter groups and counters */
int rate_counters_to_json(char *buf, size_t buflen)
{
	struct stats_json_ctx ctx;
	int rc;

	if (!buf || buflen == 0)
		return -EINVAL;

	memset(&ctx, 0, sizeof(ctx));
	ctx.buf = buf;
	ctx.buflen = buflen;
	ctx.first_ctr = true;

	rc = sj_append_fmt(&ctx, "{\"rate_counters\":[");
	if (rc)
		return rc;

	rc = rate_ctr_for_each_group(sj_rate_ctr_group_flat_handler, &ctx);
	if (rc)
		return rc;

	rc = sj_append_fmt(&ctx, "]}");
	if (rc)
		return rc;

	return (int)ctx.off;
}

/* Build JSON for 'show transceivers' by enumerating transceivers */
int transceiver_to_json(char *buf, size_t buflen)
{
	struct stats_json_ctx ctx;
	int rc;

	if (!buf || buflen == 0)
		return -EINVAL;

	memset(&ctx, 0, sizeof(ctx));
	ctx.buf = buf;
	ctx.buflen = buflen;

	rc = sj_append_fmt(&ctx, "{\"transceivers\":[");
	if (rc)
		return rc;

	rc = sj_transceiver_handler_ll(&ctx);
	if (rc)
		return rc;

	rc = sj_append_fmt(&ctx, "]}");
	if (rc)
		return rc;

	return (int)ctx.off;
}

/* Build JSON for WebSDR runtime counters */
int websdr_to_json(char *buf, size_t buflen)
{
	struct stats_json_ctx ctx;
	int rc;

	if (!buf || buflen == 0)
		return -EINVAL;

	memset(&ctx, 0, sizeof(ctx));
	ctx.buf = buf;
	ctx.buflen = buflen;

	rc = sj_append_fmt(&ctx,
		"{\"sdr_send_ts\":%" PRIu64 ",\"g_last_ts\":%" PRIu64
		",\"wptr\":%u,\"rptr\":%u,\"total_tx_late\":%u,\"start_fn\":%u}",
		sdr_send_ts, g_last_ts, wptr, rptr, total_tx_late, start_fn);
	if (rc)
		return rc;

	return (int)ctx.off;
}
