#include <haproxy/stats-json.h>

#include <stdio.h>

#include <haproxy/applet.h>
#include <haproxy/buf.h>
#include <haproxy/chunk.h>
#include <haproxy/stats.h>

/* Emits an encoding of the field type as JSON.
  * Returns non-zero on success, 0 if the buffer is full.
  */
static int stats_emit_json_field_tags(struct buffer *out, const struct field *f)
{
	const char *origin, *nature, *scope;
	int old_len;

	switch (field_origin(f, 0)) {
	case FO_METRIC:  origin = "Metric";  break;
	case FO_STATUS:  origin = "Status";  break;
	case FO_KEY:     origin = "Key";     break;
	case FO_CONFIG:  origin = "Config";  break;
	case FO_PRODUCT: origin = "Product"; break;
	default:         origin = "Unknown"; break;
	}

	switch (field_nature(f, 0)) {
	case FN_GAUGE:    nature = "Gauge";    break;
	case FN_LIMIT:    nature = "Limit";    break;
	case FN_MIN:      nature = "Min";      break;
	case FN_MAX:      nature = "Max";      break;
	case FN_RATE:     nature = "Rate";     break;
	case FN_COUNTER:  nature = "Counter";  break;
	case FN_DURATION: nature = "Duration"; break;
	case FN_AGE:      nature = "Age";      break;
	case FN_TIME:     nature = "Time";     break;
	case FN_NAME:     nature = "Name";     break;
	case FN_OUTPUT:   nature = "Output";   break;
	case FN_AVG:      nature = "Avg";      break;
	default:          nature = "Unknown";  break;
	}

	switch (field_scope(f, 0)) {
	case FS_PROCESS: scope = "Process"; break;
	case FS_SERVICE: scope = "Service"; break;
	case FS_SYSTEM:  scope = "System";  break;
	case FS_CLUSTER: scope = "Cluster"; break;
	default:         scope = "Unknown"; break;
	}

	old_len = out->data;
	chunk_appendf(out, "\"tags\":{"
			    "\"origin\":\"%s\","
			    "\"nature\":\"%s\","
			    "\"scope\":\"%s\""
			   "}", origin, nature, scope);
	return !(old_len == out->data);
}

/* Limit JSON integer values to the range [-(2**53)+1, (2**53)-1] as per
 * the recommendation for interoperable integers in section 6 of RFC 7159.
 */
#define JSON_INT_MAX ((1ULL << 53) - 1)
#define JSON_INT_MIN (0 - JSON_INT_MAX)

/* Emits a stats field value and its type in JSON.
 * Returns non-zero on success, 0 on error.
 */
static int stats_emit_json_data_field(struct buffer *out, const struct field *f)
{
	int old_len;
	char buf[20];
	const char *type, *value = buf, *quote = "";

	switch (field_format(f, 0)) {
	case FF_EMPTY: return 1;
	case FF_S32:   type = "\"s32\"";
		       snprintf(buf, sizeof(buf), "%d", f->u.s32);
		       break;
	case FF_U32:   type = "\"u32\"";
		       snprintf(buf, sizeof(buf), "%u", f->u.u32);
		       break;
	case FF_S64:   type = "\"s64\"";
		       if (f->u.s64 < JSON_INT_MIN || f->u.s64 > JSON_INT_MAX)
			       return 0;
		       type = "\"s64\"";
		       snprintf(buf, sizeof(buf), "%lld", (long long)f->u.s64);
		       break;
	case FF_U64:   if (f->u.u64 > JSON_INT_MAX)
			       return 0;
		       type = "\"u64\"";
		       snprintf(buf, sizeof(buf), "%llu",
				(unsigned long long) f->u.u64);
		       break;
	case FF_FLT:   type = "\"flt\"";
		       flt_trim(buf, 0, snprintf(buf, sizeof(buf), "%f", f->u.flt));
		       break;
	case FF_STR:   type = "\"str\"";
		       value = field_str(f, 0);
		       quote = "\"";
		       break;
	default:       snprintf(buf, sizeof(buf), "%u", f->type);
		       type = buf;
		       value = "unknown";
		       quote = "\"";
		       break;
	}

	old_len = out->data;
	chunk_appendf(out, ",\"value\":{\"type\":%s,\"value\":%s%s%s}",
		      type, quote, value, quote);
	return !(old_len == out->data);
}

static void stats_print_proxy_field_json(struct buffer *out,
                                         const struct field *stat,
                                         const char *name,
                                         int pos,
                                         uint32_t field_type,
                                         uint32_t iid,
                                         uint32_t sid,
                                         uint32_t pid)
{
	const char *obj_type;
	switch (field_type) {
		case STATS_TYPE_FE: obj_type = "Frontend"; break;
		case STATS_TYPE_BE: obj_type = "Backend";  break;
		case STATS_TYPE_SO: obj_type = "Listener"; break;
		case STATS_TYPE_SV: obj_type = "Server";   break;
		default:            obj_type = "Unknown";  break;
	}

	chunk_appendf(out,
	              "{"
	              "\"objType\":\"%s\","
	              "\"proxyId\":%u,"
	              "\"id\":%u,"
	              "\"field\":{\"pos\":%d,\"name\":\"%s\"},"
	              "\"processNum\":%u,",
	              obj_type, iid, sid, pos, name, pid);
}

static void stats_print_rslv_field_json(struct buffer *out,
                                        const struct field *stat,
                                        const char *name,
                                        int pos)
{
	chunk_appendf(out,
	              "{"
	              "\"field\":{\"pos\":%d,\"name\":\"%s\"},",
	              pos, name);
}


/* Dumps the stats JSON header to <out> buffer. The caller is responsible for
 * clearing it if needed.
 */
void stats_dump_json_header(struct buffer *out)
{
	chunk_strcat(out, "[");
}

/* Dump all fields from <stats> into <out> using a typed "field:desc:type:value" format */
int stats_dump_fields_json(struct buffer *out,
                           const struct field *stats, size_t stats_count,
                           struct show_stat_ctx *ctx)
{
	int flags = ctx->flags;
	int domain = ctx->domain;
	int started = (ctx->field) ? 1 : 0;
	int ready_data = 0;

	if (!started && (flags & STAT_STARTED) && !chunk_strcat(out, ","))
		return 0;
	if (!started && !chunk_strcat(out, "["))
		return 0;

	for (; ctx->field < stats_count; ctx->field++) {
		int old_len;
		int field = ctx->field;

		if (!stats[field].type)
			continue;

		if (started && !chunk_strcat(out, ","))
			goto err;
		started = 1;

		old_len = out->data;
		if (domain == STATS_DOMAIN_PROXY) {
			stats_print_proxy_field_json(out, &stats[field],
			                             stat_f[domain][field].name,
			                             field,
			                             stats[ST_F_TYPE].u.u32,
			                             stats[ST_F_IID].u.u32,
			                             stats[ST_F_SID].u.u32,
			                             stats[ST_F_PID].u.u32);
		} else if (domain == STATS_DOMAIN_RESOLVERS) {
			stats_print_rslv_field_json(out, &stats[field],
			                            stat_f[domain][field].name,
			                            field);
		}

		if (old_len == out->data)
			goto err;

		if (!stats_emit_json_field_tags(out, &stats[field]))
			goto err;

		if (!stats_emit_json_data_field(out, &stats[field]))
			goto err;

		if (!chunk_strcat(out, "}"))
			goto err;
		ready_data = out->data;
	}

	if (!chunk_strcat(out, "]"))
		goto err;

	ctx->field = 0; /* we're done */
	return 1;

err:
	if (!ready_data) {
		/* not enough buffer space for a single entry.. */
		chunk_reset(out);
		if (ctx->flags & STAT_STARTED)
			chunk_strcat(out, ",");
		chunk_appendf(out, "{\"errorStr\":\"output buffer too short\"}");
		return 0; /* hard error */
	}
	/* push ready data and wait for a new buffer to complete the dump */
	out->data = ready_data;
	return 1;
}

/* Dumps the JSON stats trailer block to <out> buffer. The caller is
 * responsible for clearing it if needed.
 */
void stats_dump_json_end(struct buffer *out)
{
	chunk_strcat(out, "]\n");
}

/* Dump all fields from <stats> into <out> using the "show info json" format */
int stats_dump_json_info_fields(struct buffer *out,
                                const struct field *info,
                                struct show_stat_ctx *ctx)
{
	int started = (ctx->field) ? 1 : 0;
	int ready_data = 0;

	if (!started && !chunk_strcat(out, "["))
		return 0;

	for (; ctx->field < INF_TOTAL_FIELDS; ctx->field++) {
		int old_len;
		int field = ctx->field;

		if (!field_format(info, field))
			continue;

		if (started && !chunk_strcat(out, ","))
			goto err;
		started = 1;

		old_len = out->data;
		chunk_appendf(out,
			      "{\"field\":{\"pos\":%d,\"name\":\"%s\"},"
			      "\"processNum\":%u,",
			      field, info_fields[field].name,
			      info[INF_PROCESS_NUM].u.u32);
		if (old_len == out->data)
			goto err;

		if (!stats_emit_json_field_tags(out, &info[field]))
			goto err;

		if (!stats_emit_json_data_field(out, &info[field]))
			goto err;

		if (!chunk_strcat(out, "}"))
			goto err;
		ready_data = out->data;
	}

	if (!chunk_strcat(out, "]\n"))
		goto err;
	ctx->field = 0; /* we're done */
	return 1;

err:
	if (!ready_data) {
		/* not enough buffer space for a single entry.. */
		chunk_reset(out);
		chunk_appendf(out, "{\"errorStr\":\"output buffer too short\"}\n");
		return 0; /* hard error */
	}
	/* push ready data and wait for a new buffer to complete the dump */
	out->data = ready_data;
	return 1;
}

/* This function dumps the schema onto the stream connector's read buffer.
 * It returns 0 as long as it does not complete, non-zero upon completion.
 * No state is used.
 *
 * Integer values bounded to the range [-(2**53)+1, (2**53)-1] as
 * per the recommendation for interoperable integers in section 6 of RFC 7159.
 */
void stats_dump_json_schema(struct buffer *out)
{

	int old_len = out->data;

	chunk_strcat(out,
		     "{"
		      "\"$schema\":\"http://json-schema.org/draft-04/schema#\","
		      "\"oneOf\":["
		       "{"
			"\"title\":\"Info\","
			"\"type\":\"array\","
			"\"items\":{"
			 "\"title\":\"InfoItem\","
			 "\"type\":\"object\","
			 "\"properties\":{"
			  "\"field\":{\"$ref\":\"#/definitions/field\"},"
			  "\"processNum\":{\"$ref\":\"#/definitions/processNum\"},"
			  "\"tags\":{\"$ref\":\"#/definitions/tags\"},"
			  "\"value\":{\"$ref\":\"#/definitions/typedValue\"}"
			 "},"
			 "\"required\":[\"field\",\"processNum\",\"tags\","
				       "\"value\"]"
			"}"
		       "},"
		       "{"
			"\"title\":\"Stat\","
			"\"type\":\"array\","
			"\"items\":{"
			 "\"title\":\"InfoItem\","
			 "\"type\":\"object\","
			 "\"properties\":{"
			  "\"objType\":{"
			   "\"enum\":[\"Frontend\",\"Backend\",\"Listener\","
				     "\"Server\",\"Unknown\"]"
			  "},"
			  "\"proxyId\":{"
			   "\"type\":\"integer\","
			   "\"minimum\":0"
			  "},"
			  "\"id\":{"
			   "\"type\":\"integer\","
			   "\"minimum\":0"
			  "},"
			  "\"field\":{\"$ref\":\"#/definitions/field\"},"
			  "\"processNum\":{\"$ref\":\"#/definitions/processNum\"},"
			  "\"tags\":{\"$ref\":\"#/definitions/tags\"},"
			  "\"typedValue\":{\"$ref\":\"#/definitions/typedValue\"}"
			 "},"
			 "\"required\":[\"objType\",\"proxyId\",\"id\","
				       "\"field\",\"processNum\",\"tags\","
				       "\"value\"]"
			"}"
		       "},"
		       "{"
			"\"title\":\"Error\","
			"\"type\":\"object\","
			"\"properties\":{"
			 "\"errorStr\":{"
			  "\"type\":\"string\""
			 "}"
			"},"
			"\"required\":[\"errorStr\"]"
		       "}"
		      "],"
		      "\"definitions\":{"
		       "\"field\":{"
			"\"type\":\"object\","
			"\"pos\":{"
			 "\"type\":\"integer\","
			 "\"minimum\":0"
			"},"
			"\"name\":{"
			 "\"type\":\"string\""
			"},"
			"\"required\":[\"pos\",\"name\"]"
		       "},"
		       "\"processNum\":{"
			"\"type\":\"integer\","
			"\"minimum\":1"
		       "},"
		       "\"tags\":{"
			"\"type\":\"object\","
			"\"origin\":{"
			 "\"type\":\"string\","
			 "\"enum\":[\"Metric\",\"Status\",\"Key\","
				   "\"Config\",\"Product\",\"Unknown\"]"
			"},"
			"\"nature\":{"
			 "\"type\":\"string\","
			 "\"enum\":[\"Gauge\",\"Limit\",\"Min\",\"Max\","
				   "\"Rate\",\"Counter\",\"Duration\","
				   "\"Age\",\"Time\",\"Name\",\"Output\","
				   "\"Avg\", \"Unknown\"]"
			"},"
			"\"scope\":{"
			 "\"type\":\"string\","
			 "\"enum\":[\"Cluster\",\"Process\",\"Service\","
				   "\"System\",\"Unknown\"]"
			"},"
			"\"required\":[\"origin\",\"nature\",\"scope\"]"
		       "},"
		       "\"typedValue\":{"
			"\"type\":\"object\","
			"\"oneOf\":["
			 "{\"$ref\":\"#/definitions/typedValue/definitions/s32Value\"},"
			 "{\"$ref\":\"#/definitions/typedValue/definitions/s64Value\"},"
			 "{\"$ref\":\"#/definitions/typedValue/definitions/u32Value\"},"
			 "{\"$ref\":\"#/definitions/typedValue/definitions/u64Value\"},"
			 "{\"$ref\":\"#/definitions/typedValue/definitions/strValue\"}"
			"],"
			"\"definitions\":{"
			 "\"s32Value\":{"
			  "\"properties\":{"
			   "\"type\":{"
			    "\"type\":\"string\","
			    "\"enum\":[\"s32\"]"
			   "},"
			   "\"value\":{"
			    "\"type\":\"integer\","
			    "\"minimum\":-2147483648,"
			    "\"maximum\":2147483647"
			   "}"
			  "},"
			  "\"required\":[\"type\",\"value\"]"
			 "},"
			 "\"s64Value\":{"
			  "\"properties\":{"
			   "\"type\":{"
			    "\"type\":\"string\","
			    "\"enum\":[\"s64\"]"
			   "},"
			   "\"value\":{"
			    "\"type\":\"integer\","
			    "\"minimum\":-9007199254740991,"
			    "\"maximum\":9007199254740991"
			   "}"
			  "},"
			  "\"required\":[\"type\",\"value\"]"
			 "},"
			 "\"u32Value\":{"
			  "\"properties\":{"
			   "\"type\":{"
			    "\"type\":\"string\","
			    "\"enum\":[\"u32\"]"
			   "},"
			   "\"value\":{"
			    "\"type\":\"integer\","
			    "\"minimum\":0,"
			    "\"maximum\":4294967295"
			   "}"
			  "},"
			  "\"required\":[\"type\",\"value\"]"
			 "},"
			 "\"u64Value\":{"
			  "\"properties\":{"
			   "\"type\":{"
			    "\"type\":\"string\","
			    "\"enum\":[\"u64\"]"
			   "},"
			   "\"value\":{"
			    "\"type\":\"integer\","
			    "\"minimum\":0,"
			    "\"maximum\":9007199254740991"
			   "}"
			  "},"
			  "\"required\":[\"type\",\"value\"]"
			 "},"
			 "\"strValue\":{"
			  "\"properties\":{"
			   "\"type\":{"
			    "\"type\":\"string\","
			    "\"enum\":[\"str\"]"
			   "},"
			   "\"value\":{\"type\":\"string\"}"
			  "},"
			  "\"required\":[\"type\",\"value\"]"
			 "},"
			 "\"unknownValue\":{"
			  "\"properties\":{"
			   "\"type\":{"
			    "\"type\":\"integer\","
			    "\"minimum\":0"
			   "},"
			   "\"value\":{"
			    "\"type\":\"string\","
			    "\"enum\":[\"unknown\"]"
			   "}"
			  "},"
			  "\"required\":[\"type\",\"value\"]"
			 "}"
			"}"
		       "}"
		      "}"
		     "}");

	if (old_len == out->data) {
		chunk_reset(out);
		chunk_appendf(out,
			      "{\"errorStr\":\"output buffer too short\"}");
	}
	chunk_appendf(out, "\n");
}

/* This function dumps the schema onto the stream connector's read buffer.
 * It returns 0 as long as it does not complete, non-zero upon completion.
 * No state is used.
 */
int stats_dump_json_schema_to_buffer(struct appctx *appctx)
{
	struct show_stat_ctx *ctx = appctx->svcctx;
	struct buffer *chk = &ctx->chunk;

	chunk_reset(chk);

	stats_dump_json_schema(chk);

	if (applet_putchk(appctx, chk) == -1)
		return 0;

	return 1;
}
