<?php
/*
 +-------------------------------------------------------------------------+
 | Copyright (C) 2004-2025 The Cacti Group                                 |
 |                                                                         |
 | This program is free software; you can redistribute it and/or           |
 | modify it under the terms of the GNU General Public License             |
 | as published by the Free Software Foundation; either version 2          |
 | 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 General Public License for more details.                            |
 +-------------------------------------------------------------------------+
 | Cacti: The Complete RRDtool-based Graphing Solution                     |
 +-------------------------------------------------------------------------+
 | This code is designed, written, and maintained by the Cacti Group. See  |
 | about.php and/or the AUTHORS file for specific developer information.   |
 +-------------------------------------------------------------------------+
 | http://www.cacti.net/                                                   |
 +-------------------------------------------------------------------------+
*/

/**
 * get_rrdfile_names - this routine returns all of the RRDfiles know to Cacti
 *   so as to be processed when performing the Daily, Weekly, Monthly and Yearly
 *   average and peak calculations.
 *
 * @param $thread_id   - (int) The thread to process
 * @param $max_threads - (int) The maximum number of threads
 *
 * @return - (mixed) The RRDfile names
 */
function get_rrdfile_names($thread_id = 1, $max_threads = 1) {
	static $dsrows = [];

	if ($max_threads == 1) {
		return db_fetch_assoc('SELECT dtd.local_data_id, data_source_path, rrd_num AS dsses, GROUP_CONCAT(DISTINCT data_source_name) AS dsnames
			FROM data_template_data AS dtd
			INNER JOIN data_template_rrd AS dtr
			ON dtd.local_data_id = dtr.local_data_id
			LEFT JOIN poller_item AS pi
			ON pi.local_data_id = dtd.local_data_id
			WHERE pi.local_data_id IS NOT NULL
			AND data_source_path != ""
			AND dtd.local_data_id != 0
			GROUP BY dtd.local_data_id');
	}

	if (cacti_sizeof($dsrows)) {
		return $dsrows;
	} else {
		$dsses_total = db_fetch_cell('SELECT COUNT(DISTINCT dtd.local_data_id)
			FROM data_template_data AS dtd
			LEFT JOIN poller_item AS pi
			ON pi.local_data_id = dtd.local_data_id
			WHERE pi.local_data_id IS NOT NULL
			AND data_source_path != ""
			AND dtd.local_data_id != 0');

		$split_size = ceil($dsses_total / $max_threads);
		$start      = (($thread_id - 1) * $split_size) + 1;

		$rows = db_fetch_assoc("SELECT dtd.local_data_id, data_source_path, rrd_num AS dsses, GROUP_CONCAT(DISTINCT data_source_name) AS dsnames
			FROM data_template_data AS dtd
			INNER JOIN data_template_rrd AS dtr
			ON dtd.local_data_id = dtr.local_data_id
			LEFT JOIN poller_item AS pi
			ON pi.local_data_id = dtd.local_data_id
			WHERE pi.local_data_id IS NOT NULL
			AND data_source_path != ''
			AND dtd.local_data_id != 0
			GROUP BY dtd.local_data_id
			LIMIT $start, $split_size");

		$dsrows = $rows;

		if (isset($rows)) {
			return $rows;
		} else {
			return [];
		}
	}
}

/**
 * dsstats_debug - this simple routine prints a standard message to the console
 *   when running in debug mode.
 *
 * @param $message - (string) The message to display
 *
 * @return - NULL
 */
function dsstats_debug($message) {
	global $debug;

	if ($debug) {
		print 'DSSTATS: ' . $message . PHP_EOL;
	}
}

/**
 * dsstats_get_and_store_ds_avgpeak_values - this routine is a generic routine that takes an time interval as an
 *   input parameter and then, though additional function calls, reads the RRDfiles for the correct information
 *   and stores that information into the various database tables.
 *
 * @param $interval  - (string) either 'daily', 'weekly', 'monthly', or 'yearly'
 * @param $type      - (string) the statistics type to store
 * @param $thread_id - (int) the dsstats parallel thread id
 *
 * @return - NULL
 */
function dsstats_get_and_store_ds_avgpeak_values($interval, $type, $thread_id = 1) {
	global $total_user, $total_system, $total_real, $total_dsses;
	global $user_time, $system_time, $real_time, $rrd_files;

	$user_time   = 0;
	$system_time = 0;
	$real_time   = 0;
	$dsses       = 0;

	dsstats_debug(sprintf('Processing %s for Thread %s', $interval, $thread_id));

	$max_threads = read_config_option('dsstats_parallel');
	$mode        = read_config_option('dsstats_mode');
	$peak        = read_config_option('dsstats_peak') == 'on' ? true: false;

	if (empty($max_threads)) {
		$max_threads = 1;
		set_config_option('dsstats_parallel', '1');
	}

	if (empty($thread_id)) {
		$thread_id   = 1;
		$max_threads = 1;
	}

	$rrdfiles   = get_rrdfile_names($thread_id, $max_threads);
	$stats      = [];
	$rrd_files += cacti_sizeof($rrdfiles);

	$use_proxy  = (read_config_option('storage_location') > 0 ? true : false);

	/* open a pipe to rrdtool for writing and reading */
	if ($use_proxy) {
		$rrd_process = rrd_init(false);
	} else {
		$rrd_process = dsstats_rrdtool_init();
	}

	if (cacti_sizeof($rrdfiles)) {
		foreach ($rrdfiles as $file) {
			$dsses += $file['dsses'];

			if ($file['data_source_path'] != '') {
				$rrdfile       = str_replace('<path_rra>', CACTI_PATH_RRA, $file['data_source_path']);
				$local_data_id = $file['local_data_id'];
				$dsnames       = explode(',', $file['dsnames']);

				$stats[$local_data_id] = dsstats_obtain_data_source_avgpeak_values($local_data_id, $rrdfile, $interval, $mode, $peak, $rrd_process);
			} else {
				$data_source_name = db_fetch_cell_prepared('SELECT name_cache
					FROM data_template_data
					WHERE local_data_id = ?',
					[$file['local_data_id']]);

				cacti_log("WARNING: Data Source '$data_source_name' is damaged and contains no path.  Please delete and re-create both the Graph and Data Source.", false, 'DSSTATS');
			}
		}
	}

	if ($use_proxy) {
		rrd_close($rrd_process);
	} else {
		dsstats_rrdtool_close($rrd_process);
	}

	dsstats_write_buffer($stats, $interval, $mode);

	if (!empty($type)) {
		$total_user   += $user_time;
		$total_system += $system_time;
		$total_real   += $real_time;
		$total_dsses  += $dsses;

		set_config_option('dsstats_rrd_system_'  . $type . '_' . $thread_id, $total_system);
		set_config_option('dsstats_rrd_user_'    . $type . '_' . $thread_id, $total_user);
		set_config_option('dsstats_rrd_real_'    . $type . '_' . $thread_id, $total_real);
		set_config_option('dsstats_total_rrds_'  . $type . '_' . $thread_id, $rrd_files);
		set_config_option('dsstats_total_dsses_' . $type . '_' . $thread_id, $total_dsses);
	}
}

/**
 * dsstats_write_buffer - this routine provide bulk database insert services to the various tables that store
 *   the average and peak information for Data Sources.
 *
 * @param array  $stats_array - A multi dimensional array keyed by the local_data_id that contains both
 *   the average and max values for each internal RRDfile Data Source.
 * @param string $interval    - 'daily', 'weekly', 'monthly', and 'yearly'.  Used for determining the table to
 *   update during the dumping of the buffer.
 * @param int    $mode        - The mode of collection legacy '0' or advanced '1'
 *
 * @return - NULL
 */
function dsstats_write_buffer(&$stats_array, $interval, $mode) {
	$outbuf     = '';
	$out_length = 0;
	$i          = 1;
	$max_packet = 256000;

	// Format $stats[ldi][avg|max][rrd_name][metric] = $value
	if ($mode == 1) {
		$sql_prefix = "INSERT INTO data_source_stats_$interval
			(local_data_id, rrd_name, cf, average, peak, p95n, p90n, p75n, p50n, p25n, sum, stddev, lslslope, lslint, lslcorrel) VALUES";

		$sql_suffix = ' ON DUPLICATE KEY UPDATE
			average=VALUES(average),
			peak=VALUES(peak),
			p95n=VALUES(p95n),
			p90n=VALUES(p90n),
			p75n=VALUES(p75n),
			p50n=VALUES(p50n),
			p25n=VALUES(p25n),
			sum=VALUES(sum),
			stddev=VALUES(stddev),
			lslslope=VALUES(lslslope),
			lslint=VALUES(lslint),
			lslcorrel=VALUES(lslcorrel)';
	} else {
		$sql_prefix = "INSERT INTO data_source_stats_$interval (local_data_id, rrd_name, cf, average, peak) VALUES";

		$sql_suffix = ' ON DUPLICATE KEY UPDATE
			average=VALUES(average),
			peak=VALUES(peak)';
	}

	$overhead = strlen($sql_prefix) + strlen($sql_suffix);

	/* don't attempt to process an empty array */
	/* Format $stats[ldi][avg|max][rrd_name][metric] = $value */
	if (cacti_sizeof($stats_array)) {
		foreach ($stats_array as $local_data_id => $ldi_stats) {
			if (cacti_sizeof($ldi_stats)) {
				foreach ($ldi_stats as $cf => $cf_stats) {
					if ($cf == 'avg') {
						$mycf = '0';
					} else {
						$mycf = '1';
					}

					foreach ($cf_stats as $rrd_name => $stats) {
						if ($mode == 0) {
							$outbuf .= ($i == 1 ? ' ':', ') . "('" .
								$local_data_id      . "','" .
								$rrd_name           . "','" .
								$mycf               . "','" .
								$stats['avg']       . "','" .
								$stats['peak']      . "')";
						} else {
							$outbuf .= ($i == 1 ? ' ':', ') . "('" .
								$local_data_id      . "','" .
								$rrd_name           . "','" .
								$mycf               . "','" .
								$stats['avg']       . "','" .
								$stats['peak']      . "','" .
								$stats['p95n']      . "','" .
								$stats['p90n']      . "','" .
								$stats['p75n']      . "','" .
								$stats['p50n']      . "','" .
								$stats['p25n']      . "','" .
								$stats['sum']       . "','" .
								$stats['stddev']    . "','" .
								$stats['lslslope']  . "','" .
								$stats['lslint']    . "','" .
								$stats['lslcorrel'] . "')";
						}

						$out_length += strlen($outbuf);

						if (($out_length + $overhead) > $max_packet) {
							db_execute($sql_prefix . $outbuf . $sql_suffix);

							$outbuf     = '';
							$out_length = 0;
							$i          = 1;
						} else {
							$i++;
						}
					}
				}
			} else {
				$host_id = db_fetch_cell_prepared('SELECT host_id
					FROM data_local
					WHERE id = ?',
					[$local_data_id]);

				cacti_log(sprintf('WARNING: Problem with Data Source for Device[%s], DS[%s], Interval[%s]', $host_id, $local_data_id, ucfirst($interval)), false, 'DSSTATS');
			}
		}

		/* flush the buffer if it still has elements in it */
		if ($out_length > 0) {
			db_execute($sql_prefix . $outbuf . $sql_suffix);
		}
	}
}

/**
 * dsstats_obtain_data_source_avgpeak_values - this routine, given the rrdfile name, interval and RRDtool process
 *   pipes, will obtain the average a peak values from the RRDfile.  It does this in two steps:
 *
 *   1) It first reads the RRDfile's information header to obtain all of the internal data source names,
 *   poller interval and consolidation functions.
 *   2) Based upon the available consolidation functions, it then grabs either AVERAGE, and MAX, or just AVERAGE
 *      in the case where the MAX consolidation function is not included in the RRDfile, and then proceeds to
 *      gather data from the RRDfile for the time period in question.  It allows RRDtool to select the RRA to
 *      use by simply limiting the number of rows to be returned to the default.
 *
 *   Once it has all of the information from the RRDfile.  It then decomposes the resulting XML file to its
 *   components and then calculates the AVERAGE and MAX values from that data and returns an array to the calling
 *   function for storage into the respective database table.
 *
 * @param $local_data_id - (int) The Cacti Local Data Id
 * @param $rrdfile       - (string) The rrdfile to process
 * @param $interval      - (string) The interval type to process
 * @param $mode          - (int) If we should be collecting legacy stats or not
 * @param $peak          - (bool) If the peak should be take from the max cf
 * @param $rrd_process   - (resource) Pipes to the background RRDtool process
 *
 * @return - (mixed) An array of AVERAGE, and MAX values in an RRDfile by Data Source name
 */
function dsstats_obtain_data_source_avgpeak_values($local_data_id, $rrdfile, $interval, $mode, $peak, $rrd_process) {
	global $user_time, $system_time, $real_time;

	$use_proxy = (read_config_option('storage_location') ? true : false);

	if ($use_proxy) {
		$file_exists = rrdtool_execute("file_exists $rrdfile", true, RRDTOOL_OUTPUT_BOOLEAN, $rrd_process, 'DSSTATS');
	} else {
		clearstatcache();
		$file_exists = file_exists($rrdfile);
	}

	/* don't attempt to get information if the file does not exist */
	if ($file_exists) {
		$stats_command = db_fetch_cell_prepared('SELECT stats_command
			FROM data_source_stats_command_cache
			WHERE local_data_id = ?',
			[$local_data_id]);

		if ($stats_command == '') {
			$stats_command = dsstats_get_stats_command($local_data_id, $rrdfile, $use_proxy, $mode, $peak, $rrd_process);
		}

		if ($stats_command !== false) {
			/* change the interval to something RRDtool understands */
			switch($interval) {
				case 'daily':
					$interval = 'day';

					break;
				case 'weekly':
					$interval = 'week';

					break;
				case 'monthly':
					$interval = 'month';

					break;
				case 'yearly':
					$interval = 'year';

					break;
			}

			/* now execute the graph command */
			$stats_cmd = 'graph x --start now-1' . $interval . ' --end now ' . trim($stats_command);

			//print $stats_cmd . PHP_EOL . PHP_EOL;

			if ($use_proxy) {
				$xport_data = rrdtool_execute($stats_cmd, false, RRDTOOL_OUTPUT_STDOUT, $rrd_process, 'DSSTATS');
			} else {
				$xport_data = dsstats_rrdtool_execute($stats_cmd, $rrd_process);
			}

			/* process the xport array and return average and peak values */
			if ($xport_data != '') {
				$xport_array = explode("\n", $xport_data);

				if (cacti_sizeof($xport_array)) {
					foreach ($xport_array as $index => $line) {
						$line = trim($line);

						if ($line == '' || $line == '0x0') {
							continue;
						}

						if ($index > 0) {
							// Catch the last line
							if (str_starts_with($line, 'OK')) {
								$line  = trim($line, ' OK');
								$parts = explode(' ', $line);
								//print $line . PHP_EOL;

								foreach ($parts as $line) {
									$sparts = explode(':', $line);

									switch($sparts[0]) {
										case 'u':
											$user_time = $sparts[1];

											break;
										case 's':
											$system_time = $sparts[1];

											break;
										case 'r':
											$real_time = $sparts[1];

											break;
									}
								}

								break;
							} else {
								// Get the rrd_name and the remainder
								$parts    = explode('-', $line);
								$rrd_name = $parts[0];
								$namvalpt = $parts[1];

								// Get the combined variable and value
								$parts    = explode('=', $namvalpt);
								$variable = $parts[0];
								$value    = $parts[1];

								// Get the metric and the mode
								$parts    = explode('_', $variable);
								$metric   = $parts[0];
								$amode    = $parts[1];

								if (stripos($value, 'nan') === false) {
									$dsvalues[$amode][$rrd_name][$metric] = $value;
								} else {
									$dsvalues[$amode][$rrd_name][$metric] = 'NULL';
								}
							}
						}
					}

					return $dsvalues;
				}
			}
		}
	} elseif (($interval == 'daily') || ($interval == 'day')) {
		/* only alarm if performing the 'daily' averages */
		cacti_log("WARNING: File does not exist!  DS[$local_data_id], FILE[" . $rrdfile . ']', false, 'DSSTATS');
	}
}

function dsstats_get_stats_command($local_data_id, $rrdfile, $use_proxy, $mode, $peak, $rrd_process) {
	global $user_time, $system_time, $real_time;

	/* high speed or snail speed */
	if ($use_proxy) {
		$info = rrdtool_execute("info $rrdfile", false, RRDTOOL_OUTPUT_STDOUT, $rrd_process, 'DSSTATS');
	} else {
		$info = dsstats_rrdtool_execute("info $rrdfile", $rrd_process);
	}

	/**
	 * Get the current values of stats and peak settings to see if there is any change to purge the cache
	 */
	$temp_mode = read_config_option('dsstats_mode');
	$temp_peak = read_config_option('dsstats_peak');
	set_config_option('dsstats_temp_mode', $temp_mode);
	set_config_option('dsstats_temp_peak', $temp_peak);

	$length = strlen($info);

	$command = '';

	/* don't do anything if RRDfile did not return data */
	if ($info != '') {
		$info_array = explode("\n", $info);

		$average = false;
		$max     = false;
		$dsnames = [];

		/* figure out what is in this RRDfile.  Assume CF Uniformity as Cacti does not allow async rrdfiles.
		 * also verify the consolidation functions in the RRDfile for average and max calculations.
		 */
		if (cacti_sizeof($info_array)) {
			foreach ($info_array as $line) {
				if (str_contains($line, 'ds[')) {
					$parts  = explode(']', $line);
					$parts2 = explode('[', $parts[0]);

					$dsnames[trim($parts2[1])] = 1;
				} elseif (str_contains($line, '.cf')) {
					$parts = explode('=', $line);

					if (str_contains($parts[1], 'AVERAGE')) {
						$average = true;
					}

					if (str_contains($parts[1], 'MAX')) {
						$max = true;
					}
				} elseif (str_contains($line, 'step')) {
					$parts = explode('=', $line);

					$poller_interval = trim($parts[1]);
				}
			}
		}

		/* create the command syntax to get data */
		/* assume that an RRDfile has not more than 62 data sources */
		$defs     = 'abcdefghijklmnopqrstuvwxyz012345789ABCDEFGHIJKLMNOPQRSTUVWXYZ';
		$i        = 0;
		$j        = 0;
		$def      = '';
		$command  = '';
		$dsvalues = [];

		/* escape the file name if on Windows */
		if (CACTI_SERVER_OS != 'unix') {
			$rrdfile = str_replace(':', '\\:', $rrdfile);
		}

		/* setup the graph command by parsing through the internal data source names */
		if (cacti_sizeof($dsnames)) {
			foreach ($dsnames as $dsname => $present) {
				$mydata_avg = $defs[$j] . $defs[$i] . '_a';
				$mydata_max = $defs[$j] . $defs[$i] . '_m';

				if ($average) {
					$def     .= 'DEF:' . $mydata_avg . '="' . $rrdfile . '":' . $dsname . ':AVERAGE ';
					$command .= " VDEF:{$mydata_avg}_aa=$mydata_avg,AVERAGE PRINT:{$mydata_avg}_aa:{$dsname}-avg_avg=%lf";
					$command .= " VDEF:{$mydata_avg}_am=$mydata_avg,MAXIMUM PRINT:{$mydata_avg}_am:{$dsname}-peak_avg=%lf";
					$i++;
				}

				if ($max && $peak) {
					$def     .= 'DEF:' . $mydata_max . '="' . $rrdfile . '":' . $dsname . ':MAX ';
					$command .= " VDEF:{$mydata_max}_ma=$mydata_max,AVERAGE PRINT:{$mydata_max}_ma:{$dsname}-avg_max=%lf";
					$command .= " VDEF:{$mydata_max}_mm=$mydata_max,MAXIMUM PRINT:{$mydata_max}_mm:{$dsname}-peak_max=%lf";
					$i++;
				}

				if ($mode == 1) {
					$pt = ['95', '90', '75', '50', '25'];

					if ($average) {
						foreach ($pt as $s) {
							$command .= " VDEF:{$mydata_avg}_p{$s}n={$mydata_avg},$s,PERCENTNAN PRINT:{$mydata_avg}_p{$s}n:{$dsname}-p{$s}n_avg=%lf";
						}

						// TOTAL
						$command .= " VDEF:{$mydata_avg}_sum=$mydata_avg,TOTAL PRINT:{$mydata_avg}_sum:{$dsname}-sum_avg=%lf";

						// STDDEV
						$command .= " VDEF:{$mydata_avg}_stddev=$mydata_avg,STDEV PRINT:{$mydata_avg}_stddev:{$dsname}-stddev_avg=%lf";

						// LSLSLOPE
						$command .= " VDEF:{$mydata_avg}_lslslope=$mydata_avg,LSLSLOPE PRINT:{$mydata_avg}_lslslope:{$dsname}-lslslope_avg=%lf";

						// LSLINT
						$command .= " VDEF:{$mydata_avg}_lslint=$mydata_avg,LSLINT PRINT:{$mydata_avg}_lslint:{$dsname}-lslint_avg=%lf";

						// LSLCORREL
						$command .= " VDEF:{$mydata_avg}_lslcorrel=$mydata_avg,LSLCORREL PRINT:{$mydata_avg}_lslcorrel:{$dsname}-lslcorrel_avg=%lf";
					}

					if ($max && $peak) {
						foreach ($pt as $s) {
							$command .= " VDEF:{$mydata_max}_p{$s}n={$mydata_max},$s,PERCENTNAN PRINT:{$mydata_max}_p{$s}n:{$dsname}-p{$s}n_max=%lf";
						}

						// TOTAL
						$command .= " VDEF:{$mydata_max}_sum=$mydata_max,TOTAL PRINT:{$mydata_max}_sum:{$dsname}-sum_max=%lf";

						// STDDEV
						$command .= " VDEF:{$mydata_max}_stddev=$mydata_max,STDEV PRINT:{$mydata_max}_stddev:{$dsname}-stddev_max=%lf";

						// LSLSLOPE
						$command .= " VDEF:{$mydata_max}_lslslope=$mydata_max,LSLSLOPE PRINT:{$mydata_max}_lslslope:{$dsname}-lslslope_max=%lf";

						// LSLINT
						$command .= " VDEF:{$mydata_max}_lslint=$mydata_max,LSLINT PRINT:{$mydata_max}_lslint:{$dsname}-lslint_max=%lf";

						// LSLCORREL
						$command .= " VDEF:{$mydata_max}_lslcorrel=$mydata_max,LSLCORREL PRINT:{$mydata_max}_lslcorrel:{$dsname}-lslcorrel_max=%lf";
					}
				}

				if ($i > 50) {
					$j++;
					$i = 0;
				}
			}
		}

		$command = trim($def) . ' ' . $command;

		db_execute_prepared('REPLACE INTO data_source_stats_command_cache
			(local_data_id, stats_command)
			VALUES(?, ?)',
			[$local_data_id, $command]);

		return $command;
	}

	return false;
}

/**
 * dsstats_log_statistics - provides generic timing message to both the Cacti log and the settings
 *   table so that the statistics can be graphed as well.
 *
 * @param $type - (string) the type of statistics to log, either 'HOURLY', 'DAILY', 'BOOST' or 'MAJOR'.
 *
 * @return - NULL
 */
function dsstats_log_statistics($type) {
	global $start;

	dsstats_debug($type);

	if ($type == 'HOURLY') {
		$sub_type = '';
	} elseif ($type == 'MAJOR') {
		$sub_type = 'dchild';
	} elseif ($type == 'DAILY') {
		$sub_type = 'child';
	} elseif ($type == 'BOOST') {
		$sub_type = 'bchild';
	}

	/* take time and log performance data */
	$end = microtime(true);

	if ($sub_type != '') {
		$rrd_user = db_fetch_cell_prepared('SELECT SUM(value)
			FROM settings
			WHERE name LIKE ?',
			['dsstats_rrd_user_%' . $sub_type . '%']);

		$rrd_system = db_fetch_cell_prepared('SELECT SUM(value)
			FROM settings
			WHERE name LIKE ?',
			['dsstats_rrd_system_%' . $sub_type . '%']);

		$rrd_real = db_fetch_cell_prepared('SELECT SUM(value)
			FROM settings
			WHERE name LIKE ?',
			['dsstats_rrd_real_%' . $sub_type . '%']);

		$rrd_files = db_fetch_cell_prepared('SELECT SUM(value)
			FROM settings
			WHERE name LIKE ?',
			['dsstats_total_rrds_%' . $sub_type . '%']);

		$dsses = db_fetch_cell_prepared('SELECT SUM(value)
			FROM settings
			WHERE name LIKE ?',
			['dsstats_total_dsses_%' . $sub_type . '%']);

		$processes  = read_config_option('dsstats_parallel');

		$cacti_stats = sprintf('Time:%01.2f Type:%s Threads:%s RRDfiles:%s DSSes:%s RRDUser:%01.2f RRDSystem:%01.2f RRDReal:%01.2f', $end - $start, $type, $processes, $rrd_files, $dsses, $rrd_user, $rrd_system, $rrd_real);

		db_execute("DELETE FROM settings
			WHERE name LIKE 'dsstats_rrd_%$sub_type%'
			OR name LIKE 'dsstats_total_rrds%_$sub_type%'
			OR name LIKE 'dsstats_total_dsses%_$sub_type%'");
	} else {
		$cacti_stats = sprintf('Time:%01.2f Type:%s', $end - $start, $type);
	}

	/* take time and log performance data */
	$start = microtime(true);

	/* log to the database */
	set_config_option('stats_dsstats_' . $type, $cacti_stats);

	/* log to the logfile */
	cacti_log('DSSTATS STATS: ' . $cacti_stats , true, 'SYSTEM');
}

/**
 * dsstats_log_child_stats - logs dsstats child process information
 *
 * @param $type        - (string) The type of child, MAJOR, DAILY, BOOST
 * @param $thread_id   - (int) The parallel thread id
 * @param $total_time  - (int) The total time to collect date
 *
 * @return - NULL
 */
function dsstats_log_child_stats($type, $thread_id, $total_time) {
	$rrd_user = db_fetch_cell_prepared('SELECT SUM(value)
		FROM settings
		WHERE name LIKE ?',
		['dsstats_rrd_user_%' . $type . '_' . $thread_id . '%']);

	$rrd_system = db_fetch_cell_prepared('SELECT SUM(value)
		FROM settings
		WHERE name LIKE ?',
		['dsstats_rrd_system_%' . $type . '_' . $thread_id . '%']);

	$rrd_real = db_fetch_cell_prepared('SELECT SUM(value)
		FROM settings
		WHERE name LIKE ?',
		['dsstats_rrd_real_%' . $type . '_' . $thread_id . '%']);

	$rrd_files = db_fetch_cell_prepared('SELECT SUM(value)
		FROM settings
		WHERE name LIKE ?',
		['dsstats_total_rrds_%' . $type . '_' . $thread_id . '%']);

	$dsses = db_fetch_cell_prepared('SELECT SUM(value)
		FROM settings
		WHERE name LIKE ?',
		['dsstats_total_dsses_%' . $type . '_' . $thread_id . '%']);

	$cacti_stats = sprintf('Time:%01.2f Type:%s ProcessNumber:%s RRDfiles:%s DSSes:%s RRDUser:%01.2f RRDSystem:%01.2f RRDReal:%01.2f', $total_time, strtoupper($type), $thread_id, $rrd_files, $dsses, $rrd_user, $rrd_system, $rrd_real);

	cacti_log('DSSTATS CHILD STATS: ' . $cacti_stats, true, 'SYSTEM');
}

/**
 * dsstats_error_handler - this routine logs all PHP error transactions
 *   to make sure they are properly logged.
 *
 * @param $errno    - (int) The errornum reported by the system
 * @param $errmsg   - (string) The error message provides by the error
 * @param $filename - (string) The filename that encountered the error
 * @param $linenum  - (int) The line number where the error occurred
 * @param $vars     - (mixed) The current state of PHP variables.
 *
 * @returns - (bool) always returns true for some reason
 */
function dsstats_error_handler($errno, $errmsg, $filename, $linenum, $vars = []) {
	if (read_config_option('log_verbosity') >= POLLER_VERBOSITY_DEBUG) {
		/* define all error types */
		$errortype = [
			E_ERROR             => 'Error',
			E_WARNING           => 'Warning',
			E_PARSE             => 'Parsing Error',
			E_NOTICE            => 'Notice',
			E_CORE_ERROR        => 'Core Error',
			E_CORE_WARNING      => 'Core Warning',
			E_COMPILE_ERROR     => 'Compile Error',
			E_COMPILE_WARNING   => 'Compile Warning',
			E_USER_ERROR        => 'User Error',
			E_USER_WARNING      => 'User Warning',
			E_USER_NOTICE       => 'User Notice',
			E_STRICT            => 'Runtime Notice'
		];

		if (defined('E_RECOVERABLE_ERROR')) {
			$errortype[E_RECOVERABLE_ERROR] = 'Catchable Fatal Error';
		}

		/* create an error string for the log */
		$err = "ERRNO:'"  . $errno   . "' TYPE:'"    . $errortype[$errno] .
			"' MESSAGE:'" . $errmsg  . "' IN FILE:'" . $filename .
			"' LINE NO:'" . $linenum . "'";

		/* let's ignore some lesser issues */
		if (str_contains($errmsg, 'date_default_timezone')) {
			return;
		}

		if (str_contains($errmsg, 'Only variables')) {
			return;
		}

		/* log the error to the Cacti log */
		cacti_log('PROGERR: ' . $err, false, 'DSSTATS');
	}

	return;
}

/**
 * dsstats_poller_output - this routine runs in parallel with the cacti poller and
 *   populates the last and cache tables.  On larger systems, it should be noted that
 *   the memory overhead for the global arrays, $ds_types, $ds_last, $ds_multi
 *   could be serval hundred megabytes.  So, this should be kept in mind when running the
 *   sizing your system.
 *
 *   The routine basically loads those 4 structures into memory, and then uses them to
 *   determine what should be stored in both the Cache and the Last tables.  The 4 structures
 *   contain the following information:
 *
 *   $ds_types - The type of data source, keyed by the local_data_id and the rrd_name stored inside
 *               of the RRDfile.
 *   $ds_last  - For the COUNTER, and DERIVE DS types, the last measured and stored value.
 *   $ds_multi - For Multi Part responses, stores the mapping of the Data Input Fields to the
 *               Internal RRDfile DS names.
 *
 *   The routine loops through all poller output items and makes decisions relative to the output
 *   that should be stored into the two tables, and then bulk inserts that information once
 *   all poller items have been processed.
 *
 *   The purpose for loading then entire structures into memory at one time is to reduce the latency
 *   related to multiple database calls.  The author believed that PHP's array hashing algorithms
 *   would be as fast, if not faster, than MySQL, when considering the transaction overhead and therefore
 *   chose this method.
 *
 * @param $rrd_update_array - (mixed) The output from the poller output table to be processed by dsstats
 *
 * @return - NULL
 */
function dsstats_poller_output(&$rrd_update_array) {
	static $ds_types = [];
	static $ds_multi = [];

	/* suppress warnings */
	if (defined('E_DEPRECATED')) {
		error_reporting(E_ALL ^ E_DEPRECATED);
	} else {
		error_reporting(E_ALL);
	}

	/* install the dsstats error handler */
	set_error_handler('dsstats_error_handler');

	/* do not make any calculations unless enabled */
	if (read_config_option('dsstats_enable') == 'on') {
		if (cacti_sizeof($rrd_update_array) > 0) {
			/* we will assume a smaller than the max packet size.  This would appear to be around the sweat spot. */
			$max_packet       = '264000';

			/* initialize some variables related to the DB inserts */
			$outbuf           = '';
			$sql_cache_prefix = 'INSERT INTO data_source_stats_hourly_cache (local_data_id, rrd_name, time, `value`) VALUES';
			$sql_last_prefix  = 'INSERT INTO data_source_stats_hourly_last (local_data_id, rrd_name, `value`, calculated) VALUES';
			$sql_suffix       = ' ON DUPLICATE KEY UPDATE `value`=VALUES(`value`)';
			$sql_last_suffix  = ' ON DUPLICATE KEY UPDATE `value`=VALUES(`value`), `calculated`=VALUES(`calculated`)';
			$overhead         = strlen($sql_cache_prefix) + strlen($sql_suffix);
			$overhead_last    = strlen($sql_last_prefix) + strlen($sql_last_suffix);

			/* determine the keyvalue pairs to decide on how to store data */
			if (!cacti_sizeof($ds_types)) {
				$ds_types = array_rekey(
					db_fetch_assoc('SELECT DISTINCT data_source_name, data_source_type_id, rrd_step, rrd_maximum
						FROM data_template_rrd AS dtr
						INNER JOIN data_template_data AS dtd
						ON dtd.local_data_id = dtr.local_data_id
						WHERE dtd.local_data_id > 0'),
					'data_source_name', ['data_source_type_id', 'rrd_step', 'rrd_maximum']
				);
			}

			/* make the association between the multi-part name value pairs and the RRDfile internal
			 * data source names.
			 */
			if (!cacti_sizeof($ds_multi)) {
				$ds_multi = array_rekey(
					db_fetch_assoc('SELECT DISTINCT data_name, data_source_name
						FROM graph_templates_item AS gti
						INNER JOIN data_template_rrd AS dtr
						ON gti.task_item_id = dtr.id
						INNER JOIN data_input_fields AS dif
						ON dif.id = dtr.data_input_field_id
						WHERE dtr.data_input_field_id != 0'),
					'data_name', 'data_source_name'
				);
			}

			/* required for updating tables */
			$cache_i      = 1;
			$last_i       = 1;
			$out_length   = 0;
			$last_length  = 0;
			$lastbuf      = '';
			$cachebuf     = '';

			/* process each array */
			$n = 1;

			foreach ($rrd_update_array as $data_source) {
				if (isset($data_source['times'])) {
					foreach ($data_source['times'] as $time => $sample) {
						foreach ($sample as $ds => $value) {
							$result['local_data_id'] = $data_source['local_data_id'];
							$result['rrd_name']      = $ds;
							$result['time']          = date('Y-m-d H:i:s', $time);

							if (is_numeric($value)) {
								$result['output'] = $value;
							} elseif ($value == 'U' || strtolower($value) == 'nan') {
								$result['output'] = 'NULL';
							} else {
								$result['output'] = 'NULL';

								cacti_log('ERROR: Output from local_data_id ' .
									$data_source['local_data_id'] .
									", for RRDfile DS Name '$ds', is invalid.  " .
									"It outputs was : '" . $value . "'. " .
									'Please check your script or data input method for errors.');
							}

							$lastval = '';

							if (!isset($ds_types[$result['rrd_name']]['data_source_type_id'])) {
								$polling_interval = db_fetch_cell_prepared('SELECT rrd_step
									FROM data_template_data
									WHERE local_data_id = ?',
									[$data_source['local_data_id']]);

								$ds_type = db_fetch_cell_prepared('SELECT data_source_type_id
									FROM data_template_rrd
									WHERE local_data_id = ?',
									[$data_source['local_data_id']]);
							} else {
								$polling_interval = $ds_types[$result['rrd_name']]['rrd_step'];
								$ds_type          = $ds_types[$result['rrd_name']]['data_source_type_id'];
							}

							switch ($ds_type) {
								case 2:	// COUNTER
								case 6:	// DCOUNTER
									/* get the last values from the database for COUNTER and DERIVE data sources */
									$ds_last = db_fetch_cell_prepared('SELECT SQL_NO_CACHE `value`
									FROM data_source_stats_hourly_last
									WHERE local_data_id = ?
									AND rrd_name = ?',
										[$result['local_data_id'], $result['rrd_name']]);

									if ($ds_last == '' || $ds_last == 'NULL') {
										$currentval = 'NULL';
									} elseif ($result['output'] == 'NULL') {
										$currentval = 'NULL';
									} elseif ($result['output'] >= $ds_last) {
										/* everything is normal */
										$currentval = $result['output'] - $ds_last;
									} else {
										$max_value = $ds_types[$result['rrd_name']]['rrd_maximum'];

										/* possible overflow, see if its 32bit or 64bit */
										if ($ds_last > 4294967295) {
											$currentval = (18446744073709551615 - $ds_last) + $result['output'];
										} else {
											$currentval = (4294967295 - $ds_last) + $result['output'];
										}

										if ($max_value != 'U' && $currentval > $max_value) {
											$currentval = 'NULL';
										}
									}

									if ($currentval != 'NULL') {
										$currentval /= $polling_interval;

										if ($ds_type == 6) {
											$currentval = round($currentval, 0);
										}
									}

									$lastval = $result['output'];

									if ($ds_type == 6) {
										$lastval = round($lastval, 0);
									}

									break;
								case 3:	// DERIVE
								case 7:	// DDERIVE
									/* get the last values from the database for COUNTER and DERIVE data sources */
									$ds_last = db_fetch_cell_prepared('SELECT SQL_NO_CACHE `value`
									FROM data_source_stats_hourly_last
									WHERE local_data_id = ?
									AND rrd_name = ?', [$result['local_data_id'], $result['rrd_name']]);

									if ($ds_last == '') {
										$currentval = 'NULL';
									} elseif ($result['output'] != 'NULL') {
										$currentval = ($result['output'] - $ds_last) / $polling_interval;

										if ($ds_type == 7) {
											$currentval = round($currentval, 0);
										}
									} else {
										$currentval = 'NULL';
									}

									$lastval = $result['output'];

									if ($ds_type == 7) {
										$lastval = round($lastval, 0);
									}

									break;
								case 4:	// ABSOLUTE
									if ($result['output']          != 'NULL' &&
										$result['output']             != 'U' &&
										strtolower($result['output']) != 'nan') {
										$currentval = abs($result['output']);
										$lastval    = $currentval;
									} else {
										$currentval = 'NULL';
										$lastval    = $currentval;
									}

									break;
								case 1:	// GAUGE
									if ($result['output']          != 'NULL' &&
										$result['output']             != 'U' &&
										strtolower($result['output']) != 'nan') {
										$currentval = $result['output'];
										$lastval    = $result['output'];
									} else {
										$currentval = 'NULL';
										$lastval    = $currentval;
									}

									break;
								default:
									cacti_log("WARNING: Unknown RRDtool Data Type '" . $ds_types[$result['rrd_name']]['data_source_type_id'] . "', For '" . $result['rrd_name'] . "'", false, 'DSSTATS');

									break;
							}

							/* when doing bulk inserts, the second record is different */
							if ($cache_i == 1) {
								$cache_delim = ' ';
							} else {
								$cache_delim = ', ';
							}

							if ($last_i == 1) {
								$last_delim = ' ';
							} else {
								$last_delim = ', ';
							}

							if ($currentval == '' || $currentval == '-') {
								$currentval = 'NULL';
							}

							/* setup the output buffer for the cache first */
							$cachebuf .=
								$cache_delim . '(' .
								$result['local_data_id'] . ", '" .
								$result['rrd_name'] . "', '" .
								$result['time'] . "', " .
								$currentval . ')';

							$out_length += strlen($cachebuf);

							/* now do the last value, if applicable */
							if ($lastval != '') {
								$lastbuf .=
									$last_delim . '(' .
									$result['local_data_id'] . ", '" .
									$result['rrd_name'] . "', " .
									$lastval . ', ' .
									$currentval . ')';

								$last_i++;
								$last_length += strlen($lastbuf);
							}

							/* if we exceed our output buffer, it's time to write */
							if ((($out_length + $overhead) > $max_packet) ||
								(($last_length + $overhead_last) > $max_packet)) {
								db_execute($sql_cache_prefix . $cachebuf . $sql_suffix);

								if ($last_i > 1) {
									db_execute($sql_last_prefix . $lastbuf . $sql_last_suffix);
								}

								$cachebuf     = '';
								$lastbuf      = '';
								$out_length   = 0;
								$last_length  = 0;
								$cache_i      = 1;
								$last_i       = 1;
							} else {
								$cache_i++;
							}

							$n++;

							if (($n % 1000) == 0) {
								print '.';
							}
						}
					}
				}
			}

			if ($cache_i > 1) {
				db_execute($sql_cache_prefix . $cachebuf . $sql_suffix);
			}

			if ($last_i > 1) {
				db_execute($sql_last_prefix . $lastbuf . $sql_last_suffix);
			}
		}
	}

	/* restore original error handler */
	restore_error_handler();
}

/**
 * dsstats_boost_bottom - this routine accommodates mass updates after the boost process
 *   has completed.  The use of boost will require boost version 2.5 or above.  The idea
 *   if that daily averages will be updated on the boost cycle.
 *
 * @return - NULL
 */
function dsstats_boost_bottom() {
	global $total_user, $total_system, $total_real, $total_dsses;

	$total_user   = 0;
	$total_system = 0;
	$total_real   = 0;
	$total_dsses  = 0;

	if (read_config_option('dsstats_enable') == 'on') {
		include_once(CACTI_PATH_LIBRARY . '/rrd.php');

		/* run the daily stats. log to database to prevent secondary runs */
		set_config_option('dsstats_last_daily_run_time', date('Y-m-d G:i:s', time()));

		/* run the daily stats */
		dsstats_launch_children('bmaster');

		/* Wait for all processes to continue */
		while ($running = dsstats_processes_running('bmaster')) {
			dsstats_debug(sprintf('%s Processes Running, Sleeping for 2 seconds.', $running));
			sleep(2);
		}

		dsstats_get_and_store_ds_avgpeak_values('daily', 'child', 1);

		dsstats_log_statistics('DAILY');
	}
}

/**
 * dsstats_memory_limit - this routine increases/decreases the memory available for the script
 *   It is divided into two functions as the main dsstats poller calls this function directly
 *   as opposed to the call during the processing of poller output in the main cacti poller.
 *
 * @return - NULL
 */
function dsstats_memory_limit() {
	ini_set('memory_limit', read_config_option('dsstats_poller_mem_limit') . 'M');
}

/**
 * dsstats_poller_bottom - this routine launches the main dsstats poller so that it might
 *   calculate the Hourly, Daily, Weekly, Monthly, and Yearly averages.  It is forked independently
 *   to the Cacti poller after all polling has finished.
 *
 * @return - NULL
 */
function dsstats_poller_bottom() {
	if (read_config_option('dsstats_enable') == 'on') {
		include_once(CACTI_PATH_LIBRARY . '/poller.php');

		chdir(CACTI_PATH_BASE);

		$command_string = read_config_option('path_php_binary');

		if (read_config_option('path_dsstats_log') != '') {
			if (CACTI_SERVER_OS == 'unix') {
				$extra_args = '-q ' . CACTI_PATH_BASE . '/poller_dsstats.php >> ' . read_config_option('path_dsstats_log') . ' 2>&1';
			} else {
				$extra_args = '-q ' . CACTI_PATH_BASE . '/poller_dsstats.php >> ' . read_config_option('path_dsstats_log');
			}
		} else {
			$extra_args = '-q ' . CACTI_PATH_BASE . '/poller_dsstats.php';
		}

		exec_background($command_string, $extra_args);
	}
}

/**
 * dsstats_rrdtool_init - this routine provides a bi-directional socket based connection to RRDtool.
 *   it provides a high speed connection to rrdfile in the case where the traditional Cacti call does
 *   not when performing fetch type calls.
 *
 * @return - (mixed) An array that includes both the process resource and the pipes to communicate
 *   with RRDtool.
 */
function dsstats_rrdtool_init() {
	if (CACTI_SERVER_OS == 'unix') {
		$fds = [
			0 => ['pipe', 'r'], // stdin
			1 => ['pipe', 'w'], // stdout
			2 => ['file', '/dev/null', 'a'] // stderr
		];
	} else {
		$fds = [
			0 => ['pipe', 'r'], // stdin
			1 => ['pipe', 'w'], // stdout
			2 => ['file', 'nul', 'a'] // stderr
		];
	}

	/* set the rrdtool default font */
	if (read_config_option('path_rrdtool_default_font')) {
		putenv('RRD_DEFAULT_FONT=' . read_config_option('path_rrdtool_default_font'));
	}

	$command = read_config_option('path_rrdtool') . ' - ';

	$process = proc_open($command, $fds, $pipes);

	/* make stdin/stdout/stderr non-blocking */
	stream_set_blocking($pipes[0], 0);
	stream_set_blocking($pipes[1], 0);

	return [$process, $pipes];
}

/**
 * dsstats_rrdtool_execute - this routine passes commands to RRDtool and returns the information
 *   back to DSStats.  It is important to note here that RRDtool needs to provide an either 'OK'
 *   or 'ERROR' response across the pipe as it does not provide EOF characters to key upon.
 *   This may not be the best method and may be changed after I have a conversation with a few
 *   developers.
 *
 * @param $command      - (string) The rrdtool command to execute
 * @param $rrd_process  - (array) An array of stdin and stdout pipes to read and write data from
 *
 * @returns - (string) The output from RRDtool
 */
function dsstats_rrdtool_execute($command, $rrd_process) {
	static $broken = false;

	$use_proxy  = (read_config_option('storage_location') > 0 ? true : false);

	if ($use_proxy) {
		$output = rrdtool_execute($command, false, RRDTOOL_OUTPUT_STDOUT, $rrd_process, 'DSSTATS');
	} else {
		$stdout = '';

		if ($command == '') {
			return;
		}

		$pipes = $rrd_process[1];

		$command .= "\r\n";
		$return_code = fwrite($pipes[0], $command);

		if (is_resource($pipes[1])) {
			while (!feof($pipes[1])) {
				$stdout .= fgets($pipes[1], 4096);

				if (str_contains($stdout, 'OK')) {
					break;
				}

				if (str_contains($stdout, 'ERROR')) {
					break;
				}
			}
		} elseif (!$broken) {
			cacti_log('ERROR: RRDtool was unable to fork.  Likely RRDtool can not be found or system out of resources.  Blocking subsequent messages.', false, 'POLLER');
			$broken = true;
		}
	}

	if (strlen($stdout)) {
		return $stdout;
	}
}

/**
 * dsstats_rrdtool_close - this routine closes the RRDtool process thus also
 *   closing the pipes.
 *
 * @param  mixed $process
 * @param mixed $rrd_process
 *
 * @return - null
 */
function dsstats_rrdtool_close($rrd_process) {
	if (is_array($rrd_process)) {
		proc_terminate($rrd_process[0]);
		proc_close($rrd_process[0]);
	}
}

/**
 * dsstats_launch_children - this function will launch collector children based upon
 *   the maximum number of threads and the process type
 *
 * @param  $type - (string) The process type
 *
 * @return - null
 */
function dsstats_launch_children($type) {
	global $debug;

	$processes = read_config_option('dsstats_parallel');

	if (empty($processes)) {
		$processes = 1;
	}

	$php_binary = read_config_option('path_php_binary');

	dsstats_debug("About to launch $processes processes.");

	$sub_type = dsstats_get_subtype($type);

	for ($i = 1; $i <= $processes; $i++) {
		dsstats_debug(sprintf('Launching DSStats Process Number %s for Type %s', $i, $type));

		cacti_log(sprintf('NOTE: Launching DSStats Process Number %s for Type %s', $i, $type), false, 'BOOST', POLLER_VERBOSITY_MEDIUM);

		exec_background($php_binary, CACTI_PATH_BASE . "/poller_dsstats.php --type=$sub_type --child=$i" . ($debug ? ' --debug':''));
	}

	sleep(2);
}

/**
 * dsstats_get_subtype - this function determine the applicable
 *   sub-type (child name) and return if based upon a type
 *
 * @param $type - (string) The process type
 *
 * @return - (string) The sub type
 */
function dsstats_get_subtype($type) {
	switch($type) {
		case 'master':
		case 'pmaster':
			return 'child';

			break;
		case 'bmaster':
			return 'bchild';

			break;
		case 'dmaster':
			return 'dchild';

			break;
	}
}

/**
 * dsstats_kill_running_processes - this function is part of an interrupt
 *   handler to kill children processes when the parent is killed
 *
 * @return - NULL
 */
function dsstats_kill_running_processes() {
	global $type;

	if ($type == 'bmaster') {
		$processes = db_fetch_assoc_prepared('SELECT *
			FROM processes
			WHERE tasktype = "dsstats"
			AND taskname = "bchild"
			AND pid != ?',
			[getmypid()]);
	} else {
		$processes = db_fetch_assoc_prepared('SELECT *
			FROM processes
			WHERE tasktype = "dsstats"
			AND taskname IN ("child", "dchild")
			AND pid != ?',
			[getmypid()]);
	}

	if (cacti_sizeof($processes)) {
		foreach ($processes as $p) {
			cacti_log(sprintf('WARNING: Killing DSStats %s PID %d due to another due to signal or overrun.', ucfirst($p['taskname']), $p['pid']), false, 'BOOST');
			posix_kill($p['pid'], SIGTERM);

			unregister_process($p['tasktype'], $p['taskname'], $p['taskid'], $p['pid']);
		}
	}
}

/**
 * dsstats_processes_running - given a type, determine the number
 *   of sub-type or children that are currently running
 *
 * @param $type - (string) The process type
 *
 * @return - (int) The number of running processes
 */
function dsstats_processes_running($type) {
	$sub_type = dsstats_get_subtype($type);

	$running = db_fetch_cell_prepared('SELECT COUNT(*)
		FROM processes
		WHERE tasktype = "dsstats"
		AND taskname = ?',
		[$sub_type]);

	if ($running == 0) {
		return 0;
	}

	return $running;
}

/**
 * dsstats_get_best_partition - given a time range, determine the
 *   best partition to use to gather graph statistics
 *
 * @param  int      start_time as a unix timestamp
 * @param  int      end_time as a unix timestamp
 * @param mixed $start_time
 * @param mixed $end_time
 *
 * @return string   The table name of the best partition
 */
function dsstats_get_best_partition($start_time, $end_time = 0) {
	if ($end_time == 0) {
		$end_time = time();
	}

	$cur_time = time();

	$days = ceil(($end_time - $start_time) / 86400);

	if ($days <= 1) {
		// Daily partition
		$desired_suffix = '_v' . date('Y', $start_time) . substr('000' . date('z'), -3);
		$current_suffix = '_v' . date('Y', $cur_time)   . substr('000' . date('z', $cur_time), -3);

		if ($desired_suffix == $current_suffix) {
			return 'data_source_stats_daily';
		}

		if (db_table_exists('data_source_stats_daily' . $desired_suffix)) {
			return 'data_source_stats_daily' . $desired_suffix;
		} else {
			return 'data_source_stats_daily';
		}
	} elseif ($days > 1 && $days <= 7) {
		// Weekly partition
		$desired_suffix = '_v' . date('Y', $start_time) . substr('000' . date('W'), -3);
		$current_suffix = '_v' . date('Y', $cur_time)   . substr('000' . date('W', $cur_time), -3);

		if ($desired_suffix == $current_suffix) {
			return 'data_source_stats_weekly';
		}

		if (db_table_exists('data_source_stats_weekly' . $desired_suffix)) {
			return 'data_source_stats_weekly' . $desired_suffix;
		} else {
			return 'data_source_stats_weekly';
		}
	} elseif ($days > 7 && $days <= 30) {
		// Monthly partition
		$desired_suffix = '_v' . date('Y', $start_time) . substr('000' . date('m'), -3);
		$current_suffix = '_v' . date('Y', $cur_time)   . substr('000' . date('m', $cur_time), -3);

		if ($desired_suffix == $current_suffix) {
			return 'data_source_stats_monthly';
		}

		if (db_table_exists('data_source_stats_monthly' . $desired_suffix)) {
			return 'data_source_stats_monthly' . $desired_suffix;
		} else {
			return 'data_source_stats_monthly';
		}
	} else {
		// Yearly partition
		$desired_suffix = '_v' . date('Y', $start_time);
		$current_suffix = '_v' . date('Y', $cur_time);

		if ($desired_suffix == $current_suffix) {
			return 'data_source_stats_yearly';
		}

		if (db_table_exists('data_source_stats_yearly' . $desired_suffix)) {
			return 'data_source_stats_yearly' . $desired_suffix;
		} else {
			return 'data_source_stats_yearly';
		}
	}

	return 'data_source_stats_daily';
}
