/*
 * FCP report generators
 *
 * Utility functions
 *
 * Copyright IBM Corp. 2008, 2024
 *
 * s390-tools is free software; you can redistribute it and/or modify
 * it under the terms of the MIT license. See LICENSE for details.
 */

#define __STDC_LIMIT_MACROS
#include <stdint.h>
#include <assert.h>
#include <stdlib.h>
#include <limits.h>

#include "ziorep_utils.hpp"
#include "ziorep_cfgreader.hpp"

extern "C" {
	#include "ziomon_msg_tools.h"
}

extern const char *toolname;
extern int verbose;



/**
 * Read all essential data from the files,
 * including the headers, timestamp of first message
 * and a DeviceFilter for all available devices.
 * Note that 'agg' is NULL if not available and must
 * be free()'d otherwise. */
static int get_initial_data(const char *filename, struct file_header *f_hdr,
			    struct aggr_data **agg,
			    DeviceFilter &dev_filt, ConfigReader &cfg)
{
	FILE *fp;
	int rc = 0;
	__u64 begin;

	if (open_data_files(&fp, filename, f_hdr, agg))
		return -1;
	close_data_files(fp);

	/*
	 * Retrieve first real frame
	 */
	MsgTypeFilter msgtype_filter;
	NoopCollapser	nop_col;
	Frameset frameset(&nop_col);
	list<MsgTypes> type_flt;
	type_flt.push_back(ioerr);

	// we retrieve the first interval only, and eventually the .agg data
	// as well
	if (*agg)
		begin = (*agg)->end_time;
	else
		begin = f_hdr->begin_time;

	Framer framer(begin,
		      begin + f_hdr->interval_length,
		      f_hdr->interval_length, &type_flt,
		      (DeviceFilter*)NULL, filename, &rc);
	vector <struct ioerr_cnt*> ioerrs;
	do {
		if ( framer.get_next_frameset(frameset) != 0 ) {
			fprintf(stderr, "%s: Could not read"
				" any frames in %s%s\n", toolname, filename,
				DACC_FILE_EXT_LOG);
			return -2;
		}

		/*
		 * Construct a DeviceFilter with all devices.
		 * NOTE: The very first ioerr msg might already have been moved to the .agg
		 * file - hence we have to consider the .agg data as well!
		 */
		ioerrs = frameset.get_ioerr_stats();
		rc = 0;
		for (vector<struct ioerr_cnt*>::const_iterator i = ioerrs.begin();
		      i != ioerrs.end(); ++i) {
			vverbose_msg("    add device: hctl=[%d:%d:%d:%d], mm=%d\n",
				    (*i)->identifier.host, (*i)->identifier.channel,
				    (*i)->identifier.target, (*i)->identifier.lun,
				    cfg.get_mm_by_ident(&(*i)->identifier, &rc));
			dev_filt.add_device(cfg.get_mm_by_ident(&(*i)->identifier, &rc), &(*i)->identifier);
			if (rc)
				return -1;
		}
	} while ( frameset.is_aggregated() && !ioerrs.size());

	if (dev_filt.get_host_id_list().size() == 0 || dev_filt.get_mm_list().size() == 0) {
		fprintf(stderr, "%s: Could not retrieve initial data"
			" - data files corrupted or broken, or the .agg file is missing.\n",
			toolname);
		return -1;
	}

	verbose_msg("retrieve initial data FINISHED\n");

	return 0;
}


int add_all_devices(ConfigReader &cfg, DeviceFilter &dev_filt)
{
	int rc = 0;
	list<__u32> lst;

	cfg.get_unique_mms(lst);
	for (list<__u32>::const_iterator i = lst.begin();
	      i != lst.end(); ++i) {
		dev_filt.add_device(*i, cfg.get_ident_by_mm_internal(*i, &rc));
		assert(rc == 0);
	}

	return 0;
}


int get_all_devices(const char *filename, DeviceFilter &dev_filt,
		    ConfigReader &cfg)
{
	struct file_header f_hdr;
	struct aggr_data *agg;
	int rc = 0;

	rc = get_initial_data(filename, &f_hdr, &agg, dev_filt, cfg);
	discard_aggr_data_struct(agg);
	free(agg);

	return rc;
}


#define BUF_SZ		256

const char* print_time_formatted(__u64 timestamp)
{
	time_t t = timestamp;
	static char buf[BUF_SZ];

	strftime(buf, BUF_SZ - 1, "%Y-%m-%d %H:%M:%S", localtime(&t));

	return buf;
}


const char* print_time_formatted_short(__u64 timestamp)
{
	time_t t = timestamp;
	static char buf[BUF_SZ];

	strftime(buf, BUF_SZ - 1, "%H:%M:%S", localtime(&t));

	return buf;
}


// adjust lower timestamp to last frame boundary that was touched
static __u64 round_lower_boundary(__u64 time, __u64 begin,
			       __u32 interval_length)
{
	__u64 tmp = (time - begin)%interval_length;

	if (tmp)
		time += (interval_length - tmp);

	return time;
}


// adjust upper timestamp to last frame boundary that was touched
static __u64 round_upper_boundary(__u64 time, __u64 begin,
				     __u32 interval_length)
{
	__u64 tmp = (time - begin)%interval_length;

	if (tmp)
		time -= tmp;

	return time;
}


/**
 * The times will be adjusted to respective exakt frame boundaries.
 * The end time is only ever used to determine whether we are done - we stop
 * in case the begin of the timeframe is later than the end date.
 * Note that in case we scratch into the .agg data, we
 * (a) take all of it
 * (b) synonymously associate the .agg data with the timestamp of its
 *     final frame. This is _utterly_ important, especially in context
 *     with the interval: If it is user adjusted, and the date range touches
 *     the .agg data, we adjust the begin to the .agg data's final frame's
 *     timestamp, and make sure that when we process the agg data, the next
 *     frame is 1 unit of the original interval length away. Only after we
 *     have shifted into the 'regular' .log data can we apply any user-set
 *     interval length.
 * */
int adjust_timeframe(const char *filename, __u64 *begin, __u64 *end,
		     __u32 *interval)
{
	struct file_header f_hdr;
	struct aggr_data *agg = NULL;
	FILE *fp;
	time_t t;
	int rc = 0;

	verbose_msg("adjust timeframe:\n");

	// check begin and end time
	if (*begin > *end) {
		fprintf(stderr, "%s: Start time must be"
			" prior to end time.\n", toolname);
		rc = -1;
		goto out;
	}

	if (open_data_files(&fp, filename, &f_hdr, &agg)) {
		rc = -1;
		goto out;
	}
	close_data_files(fp);

	// check if begin scratches into .agg data
	// if so, we use the _end_ time of the .agg frame
	if (*begin == 0 && agg) {
		verbose_msg("    begin time: take from .agg data\n");
		*begin = agg->end_time;
	}
	else if (agg && *begin < agg->begin_time) {
		fprintf(stderr, "%s: Warning: Begin of timeframe is before"
			" earliest available data, which is %s.\n", toolname,
			print_time_formatted(agg->begin_time));
		*begin = agg->end_time;
		verbose_msg("    begin time: adjust to begin from .agg data\n");
	}
	else if (agg && *begin <= agg->end_time) {
		*begin = agg->end_time;
		verbose_msg("    begin time: set to end of .agg data\n");
	}
	else if (*begin < f_hdr.begin_time) {
		*begin = f_hdr.begin_time;
		verbose_msg("    begin time: adjust to begin of .log data\n");
	}
	else if (*begin > f_hdr.end_time) {
		fprintf(stderr, "%s: Begin of timeframe is past the"
			" end of available data, which is %s.\n",
			toolname, print_time_formatted(f_hdr.end_time));
		rc = -1;
		goto out;
	}
	else {
		verbose_msg("    begin time: round to nearest boundary\n");
		*begin = round_lower_boundary(*begin, f_hdr.begin_time, f_hdr.interval_length);
	}
	t = *begin;
	verbose_msg("    begin time set to: %s", ctime(&t));

	if (*end == UINT64_MAX) {
		*end = f_hdr.end_time;
		verbose_msg("    end time  : take from .log data\n");
	}
	else if (agg && *end < agg->begin_time) {
		fprintf(stderr, "%s: End of timeframe is prior to earliest"
			" available data, which is %s.\n", toolname,
			print_time_formatted(agg->begin_time));
		rc = -1;
		goto out;
	}
	else if (agg && *end < agg->end_time) {
		*end = agg->end_time;
		verbose_msg("    end time  : take from .agg data\n");
	}
	else if (!agg && *end < f_hdr.begin_time) {
		fprintf(stderr, "%s: End of timeframe is prior to earliest"
			" available data, which is %s.\n", toolname,
			print_time_formatted(f_hdr.begin_time));
		rc = -1;
		goto out;
	}
	else if (*end > f_hdr.end_time) {
		fprintf(stderr, "%s: Warning: End of timeframe is after"
			" latest available data, which is %s.\n", toolname,
			print_time_formatted(f_hdr.end_time));
		*end = f_hdr.end_time;
		verbose_msg("    end time  : adjust to end of .log data\n");
	}
	else {
		*end = round_upper_boundary(*end, f_hdr.begin_time,
					       f_hdr.interval_length);
		verbose_msg("    end time  : round to nearest boundary\n");
	}
	t = *end;
	verbose_msg("    end time set to  : %s", ctime(&t));
	assert((*end - *begin) % f_hdr.interval_length == 0);

	if (*interval == UINT32_MAX) {
		*interval = f_hdr.interval_length;
		verbose_msg("using original interval length: %lus\n",
			    (long unsigned int)*interval);
	}
	/* the exact frame boundaries don't include the length of the very
	   first interval, so we have to add one more to our calculations */
	if (*interval && (*end - *begin + f_hdr.interval_length) % *interval != 0) {
		// cut off rest in case of user-set interval
		*end -= (*end - *begin) % *interval + f_hdr.interval_length;
		t = *end;
		verbose_msg("    cut off at : %s", ctime(&t));
	}

	// check if the interval is correct
	if (*interval % f_hdr.interval_length) {
		fprintf(stderr, "%s: Data aggregation interval %lu"
			" is incompatible with source data. Please use"
			" a multiple of %lu and try again.\n", toolname,
			(long unsigned int)*interval,
			(long unsigned int)(f_hdr.interval_length));
		rc = -1;
		goto out;
	}

out:
	if (agg) {
		discard_aggr_data_struct(agg);
		free(agg);
	}

	return rc;
}


int print_report(FILE *fp, __u64 begin, __u64 end, __u32 interval,
				char *filename, __u64 topline,
				list<MsgTypes> *filter_types,
				DeviceFilter &dev_filter, Collapser &col,
				Printer &printer)
{
	int frames_printed = 0;
	bool first_time = true;
	time_t t;
	int rc = 0;
	Frameset frameset(&col);
	Framer framer(begin, end, interval,
		      filter_types, &dev_filter,
		      filename, &rc);

	if (rc)
		return -1;

	if (topline && printer.print_csv()) {
		fprintf(stderr, "%s: Warning: Cannot use '-t' with CSV mode,"
			" ignoring\n", toolname);
		topline = 0;
	}

	verbose_msg("print report for:\n");
	t = (time_t)begin;
	verbose_msg("    begin    : %s", ctime(&t));
	t = (time_t)end;
	verbose_msg("    end      : %s", (end == UINT64_MAX ? "-\n" : ctime(&t)));
	verbose_msg("    interval : %lu\n", (long unsigned int)interval);
	verbose_msg("    topline  : %llu\n", (long long unsigned int)topline);
	verbose_msg("    csv mode : %d\n", printer.print_csv());

	while ( (rc = framer.get_next_frameset(frameset, true)) == 0 ) {
		vverbose_msg("printing frameset %d\n", frames_printed);
		if (first_time || (topline && frames_printed % topline == 0)) {
			first_time = false;
			printer.print_topline(fp);
		}
		if (printer.print_frame(fp, frameset, dev_filter) < 0)
			return -1;
		++frames_printed;
	}

	if (rc > 0)
		return frames_printed;

	return rc;
}


int print_summary_report(FILE *fp, char *filename, ConfigReader &cfg)
{
	int rc = 0;
	int lrc=0;
	struct file_header f_hdr;
	struct aggr_data *a_hdr;
	DeviceFilter dev_filt;

	if (get_initial_data(filename, &f_hdr, &a_hdr, dev_filt, cfg))
		return -1;

	rc += fprintf(fp, "Data Summary\n");
	rc += fprintf(fp, "------------\n");

	rc += fprintf(fp, "Aggregated range: ");
	if (a_hdr) {
		rc += fprintf(fp, "%s to ",
			      print_time_formatted(a_hdr->begin_time - f_hdr.interval_length));
		rc += fprintf(fp, "%s\n",
			      print_time_formatted(a_hdr->end_time));
	}
	else
		rc += fprintf(fp, "none\n");

	discard_aggr_data_struct(a_hdr);
	free(a_hdr);
	a_hdr = NULL;

	rc += fprintf(fp, "Detailed range:   %s to ",
		      print_time_formatted(f_hdr.begin_time - f_hdr.interval_length));
	rc += fprintf(fp, "%s\n", print_time_formatted(f_hdr.end_time));
	rc += fprintf(fp, "Interval length:  %d seconds\n",
		      f_hdr.interval_length);

	list<__u32> disks = dev_filt.get_mm_list();
	list<__u32> host_ids  = dev_filt.get_host_id_list();
	int first = 1;
	const char *frmt;
	for (list<__u32>::const_iterator i = host_ids.begin();
	      i != host_ids.end(); ++i) {
		if (first) {
			frmt = "HBA/CHPID:        %x.%x.%04x/%x\n";
			first = 0;
		}
		else
			frmt = "                  %x.%x.%04x/%x\n";
		rc += fprintf(fp, frmt, ZIOREP_BUSID_UNPACKED(cfg.get_devno_by_host_id(*i, &lrc)),
			      cfg.get_chpid_by_host_id(*i, &lrc));
		if (lrc)
			return -1;
	}

	first = 1;
	for (list<__u32>::const_iterator i = disks.begin();
	      i != disks.end(); ++i) {
		if (first) {
			frmt = "WWPN/LUN (dev):   0x%016Lx/0x%016Lx (%s)\n";
			first = 0;
		}
		else
			frmt = "                  0x%016Lx/0x%016Lx (%s)\n";
		rc += fprintf(fp, frmt, cfg.get_wwpn_by_mm_internal(*i, &lrc),
			      cfg.get_lun_by_mm_internal(*i, &lrc),
			      cfg.get_dev_by_mm_internal(*i, &lrc));
		if (lrc)
			return -1;
	}

	return rc;
}

/* Calculates seconds since 1970 _without_ caring for daylight
   savings time (comtrary to mktime() et al).
   It does not care for leap years and the like, which is OK,
   since we use it in a very narrow scenario: To calculate any
   daylight savings time related shifts.
   Hence: Dont't use if you're not sure what you are doing... */
static __u64 secs_since_1970(const struct tm *t) {
	__u64 res = 0;
	res += t->tm_sec;
	res += 60 * t->tm_min;
	res += 3600 * t->tm_hour;
	res += 86400 * t->tm_yday;
	res += 86400 * 365 * t->tm_year;

	return res;
}


int get_datetime_val(const char *str, __u64 *tgt)
{
	struct tm t, t_old;
	char *ret;

	// strptime only sets
	memset(&t, 0, sizeof(struct tm));
	ret = strptime(str, "%Y-%m-%d %H:%M", &t);
	if (ret == NULL || *ret != '\0') {
		ret = strptime(str, "%Y-%m-%d %H:%M:%S", &t);
		if (ret == NULL || *ret != '\0') {
			fprintf(stderr, "%s: Could not parse date %s."
				" Please use format as specified in"
				" man-page\n", toolname, str);
			return -1;
		}
	}
	t_old = t;
	*tgt = mktime(&t);
	// if daylight savings time applies, 't' has been adjusted,
	// so we have to correct
	if (t_old.tm_hour != t.tm_hour)
		*tgt -= secs_since_1970(&t) - secs_since_1970(&t_old);
	verbose_msg("datetime value from user after translation: %s", ctime((const time_t *)tgt));

	return 0;
}


int parse_topline_arg(char *str, __u64 *arg)
{
	char *p;

	*arg = strtoull(str, &p, 0);
	if (*arg == ULLONG_MAX) {
		fprintf(stderr, "%s: Cannot convert"
			" %s, over/underflow occurred. Try a smaller/larger"
			" value.\n", toolname, str);
		return -1;
	}
	if (*p != '\0') {
		fprintf(stderr, "%s: Non-numeric"
			" characters in argument '%s' to option '-t'. Make"
			" sure to use only numeric characters.\n", toolname,
			str);
		return -1;
	}

	return 0;
}

FILE* open_csv_output_file(const char *filename, const char *extension,
			   int *rc)
{
	char *tmp;
	FILE *fp = NULL;

	*rc = 0;

	tmp = (char*)malloc(strlen(filename) + strlen(extension) + 1);

	sprintf(tmp, "%s%s", filename, extension);
	fp = fopen(tmp, "w");

	if (!fp) {
		fprintf(stdout, "%s: Could not open file %s. Make sure that you"
			" have sufficient permissions and try again.\n", toolname, tmp);
		*rc = -1;
	}
	else
		fprintf(stdout, "Exporting data in CSV format to %s\n", tmp);

	free(tmp);

	return fp;
}

/**
 * parse_pchid_str - parse PCHID from string to 32-bit number
 * @pchid_str: PCHID string
 * @pchid: 32-bit PCHID number
 *
 * Reads PCHID string, checks for corner cases, converts to 32-bit
 * PCHID number, checks for errors, writes resulted PCHID to specified
 * location.
 */
int parse_pchid_str(const char *const pchid_str, __u32 *const pchid)
{
	unsigned long parsed;
	char *end;

	if (strcmp(pchid_str, "n/a") == 0) {
		parsed = ZIOREP_PCHID_NA;
		goto out;
	}

	parsed = strtoul(pchid_str, &end, 16);
	if (parsed > 0xffff) {
		fprintf(stderr, "%s: PCHID %s exceeds maximum possible value for a PCHID.\n",
			toolname, pchid_str);
		return -1;
	} else if (parsed == 0 && end == &pchid_str[0]) {
		fprintf(stderr, "%s: PCHID %s could not be converted.\n", toolname, pchid_str);
		return -1;
	}

out:
	*pchid = (__u32)parsed;
	return 0;
}
