scenario.c

/*
 * Proxy Pool Governor — scenario (.pps) file reader/writer.
 *
 * Copyright (C) 2026  SWGY, Inc.
 *
 * 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 <https://www.gnu.org/licenses/>.
 *
 * SPDX-License-Identifier: AGPL-3.0-or-later
 */

#include "scenario.h"

#include <errno.h>
#include <stdlib.h>
#include <string.h>

static int
parse_header(Scenario *s)
{
	char line[1024];
	int in_pools = 0, in_services = 0;
	size_t expect_pools = 0, expect_services = 0;
	size_t pool_count = 0, service_count = 0;

	while (fgets(line, sizeof(line), s->fp) != NULL) {
		/* Strip newline */
		size_t len = strlen(line);
		if (len > 0 && line[len-1] == '\n')
			line[--len] = '\0';
		if (len > 0 && line[len-1] == '\r')
			line[--len] = '\0';

		/* Skip empty lines and comments */
		if (len == 0 || line[0] == '#')
			continue;

		/* Data section marker */
		if (strcmp(line, "---") == 0) {
			s->in_data_section = 1;
			break;
		}

		/* Directive */
		if (line[0] == '@') {
			in_pools = 0;
			in_services = 0;

			if (strncmp(line, "@pools ", 7) == 0) {
				expect_pools = (size_t)atoi(line + 7);
				in_pools = 1;
				pool_count = 0;
			} else if (strncmp(line, "@services ", 10) == 0) {
				expect_services = (size_t)atoi(line + 10);
				in_services = 1;
				service_count = 0;
			} else if (strncmp(line, "@records ", 9) == 0) {
				s->num_records = (size_t)atoi(line + 9);
			}
			/* Unknown directives ignored for forward compat */
			continue;
		}

		/* ID-name mapping lines */
		if (in_pools && pool_count < expect_pools) {
			int id;
			char name[SCENARIO_MAX_NAME];
			if (sscanf(line, "%d %63s", &id, name) == 2) {
				if (id >= 0 && id < SCENARIO_MAX_POOLS) {
					strncpy(s->pool_names[id], name,
					    SCENARIO_MAX_NAME - 1);
					s->pool_names[id][SCENARIO_MAX_NAME-1] = '\0';
					pool_count++;
					if (id >= (int)s->num_pools)
						s->num_pools = id + 1;
				}
			}
			continue;
		}

		if (in_services && service_count < expect_services) {
			int id;
			char name[SCENARIO_MAX_NAME];
			if (sscanf(line, "%d %63s", &id, name) == 2) {
				if (id >= 0 && id < SCENARIO_MAX_SERVICES) {
					strncpy(s->service_names[id], name,
					    SCENARIO_MAX_NAME - 1);
					s->service_names[id][SCENARIO_MAX_NAME-1] = '\0';
					service_count++;
					if (id >= (int)s->num_services)
						s->num_services = id + 1;
				}
			}
			continue;
		}
	}

	if (!s->in_data_section) {
		errno = EINVAL;
		return -1;
	}

	s->header_parsed = 1;
	return 0;
}

Scenario *
scenario_open_read(const char *path)
{
	Scenario *s;
	FILE *fp;

	fp = fopen(path, "r");
	if (fp == NULL)
		return NULL;

	s = calloc(1, sizeof(*s));
	if (s == NULL) {
		fclose(fp);
		return NULL;
	}

	s->fp = fp;
	s->mode = 'r';

	if (parse_header(s) < 0) {
		fclose(fp);
		free(s);
		return NULL;
	}

	return s;
}

Scenario *
scenario_open_write(const char *path)
{
	Scenario *s;
	FILE *fp;

	fp = fopen(path, "w");
	if (fp == NULL)
		return NULL;

	s = calloc(1, sizeof(*s));
	if (s == NULL) {
		fclose(fp);
		return NULL;
	}

	s->fp = fp;
	s->mode = 'w';

	return s;
}

int
scenario_write_header(Scenario *s, size_t num_records)
{
	size_t i;

	if (s->mode != 'w')
		return -1;

	fprintf(s->fp, "# Proxy Pool Scenario v%d\n", SCENARIO_VERSION);
	fprintf(s->fp, "\n");

	fprintf(s->fp, "@pools %zu\n", s->num_pools);
	for (i = 0; i < s->num_pools; i++)
		fprintf(s->fp, "%zu %s\n", i, s->pool_names[i]);
	fprintf(s->fp, "\n");

	fprintf(s->fp, "@services %zu\n", s->num_services);
	for (i = 0; i < s->num_services; i++)
		fprintf(s->fp, "%zu %s\n", i, s->service_names[i]);
	fprintf(s->fp, "\n");

	if (num_records > 0)
		fprintf(s->fp, "@records %zu\n", num_records);
	fprintf(s->fp, "---\n");

	s->in_data_section = 1;
	s->num_records = num_records;

	return 0;
}

int
scenario_read(Scenario *s, ScenarioRecord *rec)
{
	char line[512];
	int pool, service;

	if (s->mode != 'r' || !s->in_data_section)
		return -1;

	while (fgets(line, sizeof(line), s->fp) != NULL) {
		/* Skip comments and empty lines in data section */
		if (line[0] == '#' || line[0] == '\n')
			continue;

		int n = sscanf(line,
		    "%lf %d %d %lf %lf %lf %lf %lf %lf %lf %lf %lf %lf %lf",
		    &rec->timestamp,
		    &pool,
		    &service,
		    &rec->rate_success,
		    &rec->rate_lost_race,
		    &rec->rate_302,
		    &rec->rate_timeout,
		    &rec->rate_ssl,
		    &rec->rate_other,
		    &rec->response_time,
		    &rec->avg_success,
		    &rec->avg_response_time,
		    &rec->stddev_success,
		    &rec->stddev_response_time);

		if (n != 14) {
			errno = EINVAL;
			return -1;
		}

		rec->pool = (uint8_t)pool;
		rec->service = (uint8_t)service;
		s->records_read++;
		return 1;
	}

	return 0;  /* EOF */
}

int
scenario_write(Scenario *s, const ScenarioRecord *rec)
{
	if (s->mode != 'w' || !s->in_data_section)
		return -1;

	fprintf(s->fp, "%.1f %d %d %.4f %.4f %.4f %.4f %.4f %.4f %.3f %.4f %.3f %.4f %.3f\n",
	    rec->timestamp,
	    rec->pool,
	    rec->service,
	    rec->rate_success,
	    rec->rate_lost_race,
	    rec->rate_302,
	    rec->rate_timeout,
	    rec->rate_ssl,
	    rec->rate_other,
	    rec->response_time,
	    rec->avg_success,
	    rec->avg_response_time,
	    rec->stddev_success,
	    rec->stddev_response_time);

	return 0;
}

void
scenario_close(Scenario *s)
{
	if (s == NULL)
		return;
	if (s->fp != NULL)
		fclose(s->fp);
	free(s);
}

const char *
scenario_pool_name(const Scenario *s, uint8_t id)
{
	if (id >= s->num_pools)
		return NULL;
	return s->pool_names[id];
}

const char *
scenario_service_name(const Scenario *s, uint8_t id)
{
	if (id >= s->num_services)
		return NULL;
	return s->service_names[id];
}

int
scenario_pool_id(const Scenario *s, const char *name)
{
	for (size_t i = 0; i < s->num_pools; i++) {
		if (strcmp(s->pool_names[i], name) == 0)
			return (int)i;
	}
	return -1;
}

int
scenario_service_id(const Scenario *s, const char *name)
{
	for (size_t i = 0; i < s->num_services; i++) {
		if (strcmp(s->service_names[i], name) == 0)
			return (int)i;
	}
	return -1;
}