<?php
/*
	PMPro Report
	Title: Membership Stats
	Slug: memberships

	For each report, write three functions:
	* pmpro_report_{slug}_register() to register the widget (slug and title).
	* pmpro_report_{slug}_widget()   to show up on the report homepage.
	* pmpro_report_{slug}_page()     to show up when users click on the report page widget.
*/
function pmpro_report_memberships_register( $pmpro_reports ) {
	$pmpro_reports['memberships'] = __( 'Membership Stats', 'paid-memberships-pro' );

	return $pmpro_reports;
}

add_filter( 'pmpro_registered_reports', 'pmpro_report_memberships_register' );

// queue Google Visualization JS on report page
function pmpro_report_memberships_init() {
	if ( is_admin() && isset( $_REQUEST['report'] ) && $_REQUEST['report'] == 'memberships' && isset( $_REQUEST['page'] ) && $_REQUEST['page'] == 'pmpro-reports' ) {
		wp_enqueue_script( 'corechart', plugins_url( 'js/corechart.js', plugin_dir_path( __DIR__ ) ) );
	}
}
add_action( 'init', 'pmpro_report_memberships_init' );


// widget
function pmpro_report_memberships_widget() {
	global $wpdb, $pmpro_reports;

	// get levels to show stats on first 3
	$pmpro_levels = pmpro_sort_levels_by_order( pmpro_getAllLevels( true, true ) );

	$pmpro_levels = apply_filters( 'pmpro_report_levels', $pmpro_levels );
	?>
<span id="pmpro_report_memberships" class="pmpro_report-holder">
	<table class="wp-list-table widefat fixed striped">
	<thead>
		<tr>
			<th scope="col">&nbsp;</th>
			<th scope="col"><?php esc_html_e( 'Signups', 'paid-memberships-pro' ); ?></th>
			<th scope="col"><?php esc_html_e( 'All Cancellations', 'paid-memberships-pro' ); ?></th>
		</tr>
	</thead>
	<?php
		$reports = array(
			'today'      => __( 'Today', 'paid-memberships-pro' ),
			'this month' => __( 'This Month', 'paid-memberships-pro' ),
			'this year'  => __( 'This Year', 'paid-memberships-pro' ),
			'all time'   => __( 'All Time', 'paid-memberships-pro' ),
		);

		/**
		 * Filter the periods for the memberships widget.
		 * @since 3.7
		 * @param array $reports The array of periods.
		 * @return array $reports The array of periods.
		 */
		$reports = apply_filters( 'pmpro_memberships_widget_periods', $reports );

		foreach ( $reports as $report_type => $report_name ) {
			$signups       = number_format_i18n( pmpro_getSignups( $report_type ) );
			$cancellations = number_format_i18n( pmpro_getCancellations( $report_type ) );
			?>
		<tbody>
			<tr class="pmpro_report_tr">
				<th scope="row">
					<?php if ( empty( $signups ) && empty( $cancellations ) ) { ?>
						<?php echo esc_html( $report_name ); ?>
					<?php } else { ?>
						<button class="pmpro_report_th pmpro_report_th_closed">
							<?php echo esc_html( $report_name ); ?>
						</button>
					<?php } ?>
				</th>
				<td><?php echo esc_html( $signups ); ?></td>
				<td><?php echo esc_html( $cancellations ); ?></td>
			</tr>
			<?php
				// level stats
				$count           = 0;
				$max_level_count = apply_filters( 'pmpro_admin_reports_included_levels', 3 );

			foreach ( $pmpro_levels as $level ) {
				if ( $count++ >= $max_level_count ) {
					break;
				}
				?>
				<tr class="pmpro_report_tr_sub" style="display: none;">
					<th scope="row">- <?php echo esc_html( $level->name ); ?></th>
					<td><?php echo esc_html( number_format_i18n( pmpro_getSignups( $report_type, $level->id ) ) ); ?></td>
					<td><?php echo esc_html( number_format_i18n( pmpro_getCancellations( $report_type, $level->id ) ) ); ?></td>
				</tr>
				<?php
			}
			?>
		</tbody>
			<?php
		}
		?>
	</table>
	<?php if ( function_exists( 'pmpro_report_memberships_page' ) ) { ?>
		<p class="pmpro_report-button">
			<a class="button button-primary" href="<?php echo esc_url( admin_url( 'admin.php?page=pmpro-reports&report=memberships' ) ); ?>" aria-label="<?php echo esc_attr( sprintf( __( 'View the full %s report', 'paid-memberships-pro' ), $pmpro_reports['memberships'] ) ); ?>"><?php esc_html_e( 'Details', 'paid-memberships-pro' ); ?></a>
		</p>
	<?php } ?>
</span>
<?php
}

function pmpro_report_memberships_page() {
	global $wpdb, $pmpro_currency_symbol;

	// get values from form
	if ( isset( $_REQUEST['type'] ) ) {
		$type = sanitize_text_field( $_REQUEST['type'] );
	} else {
		$type = 'signup_v_all';
	}

	if ( isset( $_REQUEST['period'] ) ) {
		$period = sanitize_text_field( $_REQUEST['period'] );
	} else {
		$period = 'monthly';
	}

	if ( ! empty( $_REQUEST['month'] ) ) {
		$month = intval( $_REQUEST['month'] );
	} else {
		$month = date_i18n( 'n' );
	}

	$thisyear = date_i18n( 'Y' );
	if ( ! empty( $_REQUEST['year'] ) ) {
		$year = intval( $_REQUEST['year'] );
	} else {
		$year = date_i18n( 'Y' );
	}

	if ( ! empty( $_REQUEST['level'] ) ) {
		if ( $_REQUEST['level'] == 'paid-levels' ) {
			$l = pmpro_report_get_levels( 'paid' ); // String of ints and commas. Already escaped for SQL.
		} elseif ( $_REQUEST['level'] == 'free-levels' ) {
			$l = pmpro_report_get_levels( 'free' ); // String of ints and commas. Already escaped for SQL.
		} else {
			$l = intval( $_REQUEST['level'] ); // Escaping for SQL.
		}
	} else {
		$l = '';
	}

	if ( ! empty( $_REQUEST['discount_code'] ) ) {
		$discount_code = intval( $_REQUEST['discount_code'] );
	} else {
		$discount_code = '';
	}

	// calculate start date and how to group dates returned from DB
	if ( $period == 'daily' ) {
		$startdate     = $year . '-' . substr( '0' . $month, strlen( $month ) - 1, 2 ) . '-01';
		$enddate       = $year . '-' . substr( '0' . $month, strlen( $month ) - 1, 2 ) . '-' . date_i18n( 't', strtotime( $startdate ) );
		$date_function = 'DAY';
	} elseif ( $period == 'monthly' ) {
		$startdate     = $year . '-01-01';
		$enddate       = strval( intval( $year ) + 1 ) . '-01-01';
		$date_function = 'MONTH';
	} elseif ( $period == 'annual' ) {
		$startdate     = '1970-01-01';  // all time
		$enddate       = strval( intval( $thisyear ) + 1 ) . '-01-01';
		$date_function = 'YEAR';
	}

	// testing or live data
	$gateway_environment = get_option( "pmpro_gateway_environment");

	// Get signups.
	$sqlQuery = "SELECT $date_function(mu.startdate) as date, COUNT(DISTINCT mu.user_id) as signups
	FROM $wpdb->pmpro_memberships_users mu ";

	if ( ! empty( $discount_code ) ) {
		$sqlQuery .= "LEFT JOIN $wpdb->pmpro_discount_codes_uses dc ON mu.user_id = dc.user_id ";
	}

	$sqlQuery .= "WHERE mu.startdate >= '" . esc_sql( $startdate ) . "' ";
	$sqlQuery .= "AND mu.startdate <= '" . esc_sql( $enddate ) . " 23:59:59' ";

	if ( ! empty( $l ) ) {
		$sqlQuery .= 'AND mu.membership_id IN(' . $l . ') '; // $l is already escaped for SQL. See declaration.
	}

	if ( ! empty( $discount_code ) ) {
		$sqlQuery .= "AND dc.code_id = '" . esc_sql( $discount_code ) . "' ";
	}

	$sqlQuery .= ' GROUP BY date ORDER BY date ';

	$dates = $wpdb->get_results( $sqlQuery );

	// Build array of dates to track.
	// We are doing this after getting the signup data in case annual is selected so that we know the first year we have data for.
	$cols = array();
	if ( $period == 'daily' ) {
		$lastday = date_i18n( 't', strtotime( $startdate, current_time( 'timestamp' ) ) );

		for ( $i = 1; $i <= $lastday; $i++ ) {
			$cols[ $i ]          = new stdClass();
			$cols[ $i ]->signups = 0;
			foreach ( $dates as $day => $date ) {
				if ( $date->date == $i ) {
					$cols[ $i ]->signups = $date->signups;
				}
			}
		}
	} elseif ( $period == 'monthly' ) {
		for ( $i = 1; $i < 13; $i++ ) {
			$cols[ $i ]          = new stdClass();
			$cols[ $i ]->date    = $i;
			$cols[ $i ]->signups = 0;
			foreach ( $dates as $date ) {
				if ( $date->date == $i ) {
					$cols[ $i ]->date    = $date->date;
					$cols[ $i ]->signups = $date->signups;
				}
			}
		}
	} elseif ( $period == 'annual' ) {
		// Get the first year we have signups for.
		$first_year = $thisyear;
		foreach ( $dates as $date ) {
			if ( $date->date < $first_year ) {
				$first_year = $date->date;
			}
		}

		for ( $i = $first_year; $i <= $thisyear; $i++ ) {
			$cols[ $i ]          = new stdClass();
			$cols[ $i ]->date    = $i;
			$cols[ $i ]->signups = 0;
			foreach ( $dates as $date ) {
				if ( $date->date == $i ) {
					$cols[ $i ]->date    = $date->date;
					$cols[ $i ]->signups = $date->signups;
				}
			}
		}
	}

	$dates = ( ! empty( $cols ) ) ? $cols : $dates;

	// Signups vs. all
	$sqlQuery = "SELECT $date_function(mu1.modified) as date, COUNT(DISTINCT mu1.user_id) as cancellations
	FROM $wpdb->pmpro_memberships_users mu1 ";

	// restrict by discount code
	if ( ! empty( $discount_code ) ) {
		$sqlQuery .= "LEFT JOIN $wpdb->pmpro_discount_codes_uses dc ON mu1.user_id = dc.user_id ";
	}

	if ( $type === 'signup_v_cancel' ) {
		$sqlQuery .= "WHERE mu1.status IN('inactive','cancelled','admin_cancelled') ";
	} elseif ( $type === 'signup_v_expiration' ) {
		$sqlQuery .= "WHERE mu1.status IN('expired') ";
	} else {
		$sqlQuery .= "WHERE mu1.status IN('inactive','expired','cancelled','admin_cancelled') ";
	}

	$sqlQuery .= "AND mu1.enddate >= '" . esc_sql( $startdate ) . "'
	AND mu1.enddate <= '" . $enddate . " 23:59:59' ";


	// restrict by level
	if ( ! empty( $l ) ) {
		$sqlQuery .= 'AND mu1.membership_id IN(' . $l . ') '; // $l is already escaped for SQL. See declaration.
	}

	if ( ! empty( $discount_code ) ) {
		$sqlQuery .= "AND dc.code_id = '" . esc_sql( $discount_code ) . "' ";
	}

	$sqlQuery .= ' GROUP BY date ORDER BY date ';

	/**
	 * Filter query to get cancellation numbers in signups vs cancellations detailed report.
	 *
	 * @since 1.8.8
	 *
	 * @param string $sqlQuery The current SQL
	 * @param string $type report type
	 * @param string $startdate Start Date in YYYY-MM-DD format
	 * @param string $enddate End Date in YYYY-MM-DD format
	 * @param int $l Level ID
	 */
	$sqlQuery = apply_filters( 'pmpro_reports_signups_sql', $sqlQuery, $type, $startdate, $enddate, $l );

	$cdates = $wpdb->get_results( $sqlQuery, OBJECT_K );

	foreach ( $dates as $day => &$date ) {
		if ( ! empty( $cdates ) && ! empty( $cdates[ $day ] ) ) {
			$date->cancellations = $cdates[ $day ]->cancellations;
		} else {
			$date->cancellations = 0;
		}
	}

	// Build CSV export link.
	$csv_export_link = admin_url( 'admin-ajax.php' );

	$csv_export_link = add_query_arg(
		array(
			'action'        => 'membership_stats_csv',
			'type'          => $type,
			'period'        => $period,
			'month'         => $month,
			'year'          => $year,
			'discount_code' => $discount_code,
			'startdate'     => $startdate,
			'enddate'       => $enddate,
			'level'         => $l,
			'nonce'         => wp_create_nonce( 'membership_stats_csv' ),
		),
		$csv_export_link
	);

	?>
	<form id="posts-filter" method="get" action="">
	<h1 class="wp-heading-inline">
		<?php esc_html_e( 'Membership Stats', 'paid-memberships-pro' ); ?>
	</h1>
	<?php if ( current_user_can( 'pmpro_reportscsv' ) ) { ?>
		<a target="_blank" href="<?php echo esc_url( $csv_export_link ); ?>" class="page-title-action pmpro-has-icon pmpro-has-icon-download"><?php esc_html_e( 'Export to CSV', 'paid-memberships-pro' ); ?></a>
	<?php } ?>
	<div class="pmpro_report-filters">
		<h3><?php esc_html_e( 'Customize Report', 'paid-memberships-pro' ); ?></h3>
		<div class="tablenav top">
			<span class="pmpro_report-filter-text"><?php echo esc_html_x( 'Show', 'Dropdown label, e.g. Show Period', 'paid-memberships-pro' ); ?></span>
			<label for="period" class="screen-reader-text"><?php esc_html_e( 'Select report time period', 'paid-memberships-pro' ); ?></label>
			<select id="period" name="period">
				<option value="daily" <?php selected( $period, 'daily' ); ?>><?php esc_html_e( 'Daily', 'paid-memberships-pro' ); ?></option>
				<option value="monthly" <?php selected( $period, 'monthly' ); ?>><?php esc_html_e( 'Monthly', 'paid-memberships-pro' ); ?></option>
				<option value="annual" <?php selected( $period, 'annual' ); ?>><?php esc_html_e( 'Annual', 'paid-memberships-pro' ); ?></option>
			</select>
			<label for="type" class="screen-reader-text"><?php esc_html_e( 'Select report type', 'paid-memberships-pro' ); ?></label>
			<select id="type" name="type">
				<option value="signup_v_all" <?php selected( $type, 'signup_v_all' ); ?>><?php esc_html_e( 'Signups vs. All Cancellations', 'paid-memberships-pro' ); ?></option>
				<option value="signup_v_cancel" <?php selected( $type, 'signup_v_cancel' ); ?>><?php esc_html_e( 'Signups vs. Cancellations', 'paid-memberships-pro' ); ?></option>
				<option value="signup_v_expiration" <?php selected( $type, 'signup_v_expiration' ); ?>><?php esc_html_e( 'Signups vs. Expirations', 'paid-memberships-pro' ); ?></option>
			</select>
			<span id="for" class="pmpro_report-filter-text"><?php esc_html_e( 'for', 'paid-memberships-pro' ); ?></span>
			<label for="month" class="screen-reader-text"><?php esc_html_e( 'Select report month', 'paid-memberships-pro' ); ?></label>
			<select id="month" name="month">
				<?php for ( $i = 1; $i < 13; $i++ ) { ?>
					<option value="<?php echo esc_attr( $i ); ?>" <?php selected( $month, $i ); ?>><?php echo esc_html( date_i18n( 'F', mktime( 0, 0, 0, $i, 2 ) ) ); ?></option>
				<?php } ?>
			</select>
			<label for="year" class="screen-reader-text"><?php esc_html_e( 'Select report year', 'paid-memberships-pro' ); ?></label>
			<select id="year" name="year">
				<?php for ( $i = $thisyear; $i > 2007; $i-- ) { ?>
					<option value="<?php echo esc_attr( $i ); ?>" <?php selected( $year, $i ); ?>><?php echo esc_html( $i ); ?></option>
				<?php } ?>
			</select>
			<span id="for" class="pmpro_report-filter-text"><?php esc_html_e( 'for', 'paid-memberships-pro' ); ?></span>
			<label for="level" class="screen-reader-text"><?php esc_html_e( 'Filter report by membership level', 'paid-memberships-pro' ); ?></label>
			<select id="level" name="level">
				<option value="" 
				<?php
				if ( ! $l ) {
					?>
					selected="selected"<?php } ?>><?php esc_html_e( 'All Levels', 'paid-memberships-pro' ); ?></option>
				<option value="paid-levels" 
				<?php
				if ( isset( $_REQUEST['level'] ) && $_REQUEST['level'] === 'paid-levels' ) {
					?>
					selected="selected" <?php } ?>><?php esc_html_e( 'All Paid Levels', 'paid-memberships-pro' ); ?></option>
				<option value="free-levels" 
				<?php
				if ( isset( $_REQUEST['level'] ) && $_REQUEST['level'] === 'free-levels' ) {
					?>
					selected="selected" <?php } ?>><?php esc_html_e( 'All Free Levels', 'paid-memberships-pro' ); ?></option>
				<?php
					$levels = $wpdb->get_results( "SELECT id, name FROM $wpdb->pmpro_membership_levels ORDER BY name" );
					$levels = pmpro_sort_levels_by_order( $levels );
				foreach ( $levels as $level ) {
					?>
					<option value="<?php echo esc_attr( $level->id ); ?>" 
												<?php
												if ( $l == $level->id ) {
													?>
						selected="selected"<?php } ?>><?php echo esc_html( $level->name ); ?></option>
					<?php
				}

				?>

			</select>
			<?php
			$sqlQuery  = "SELECT * FROM $wpdb->pmpro_discount_codes ";
			$sqlQuery .= 'ORDER BY id DESC ';
			$codes     = $wpdb->get_results( $sqlQuery, OBJECT );
			if ( ! empty( $codes ) ) {
				?>
			<label for="discount_code" class="screen-reader-text"><?php esc_html_e( 'Filter report by discount code', 'paid-memberships-pro' ); ?></label>
			<select id="discount_code" name="discount_code">
				<option value="" 
				<?php
				if ( empty( $discount_code ) ) {
					?>
					selected="selected"<?php } ?>><?php esc_html_e( 'All Codes', 'paid-memberships-pro' ); ?></option>
				<?php foreach ( $codes as $code ) { ?>
					<option value="<?php echo esc_attr( $code->id ); ?>" <?php selected( $discount_code, $code->id ); ?>><?php echo esc_html( $code->code ); ?></option>
				<?php } ?>
			</select>
			<?php } ?>
		</div> <!-- end tablenav -->
		<input type="hidden" name="page" value="pmpro-reports" />
		<input type="hidden" name="report" value="memberships" />
		<input type="submit" class="button button-primary" value="<?php esc_attr_e( 'Generate Report', 'paid-memberships-pro' ); ?>" />
	</div> <!-- end pmpro_report-filters -->
	<div class="pmpro_chart_area">
		<div id="chart_div" style="clear: both; width: 100%; height: 500px;"></div>
	</div>
	<script>
		//update month/year when period dropdown is changed
		jQuery(document).ready(function() {
			jQuery('#period').on('change',function() {
				pmpro_ShowMonthOrYear();
			});
		});

		function pmpro_ShowMonthOrYear()
		{
			var period = jQuery('#period').val();
			if(period == 'daily')
			{
				jQuery('#for').show();
				jQuery('#month').show();
				jQuery('#year').show();
			}
			else if(period == 'monthly')
			{
				jQuery('#for').show();
				jQuery('#month').hide();
				jQuery('#year').show();
			}
			else
			{
				jQuery('#for').hide();
				jQuery('#month').hide();
				jQuery('#year').hide();
			}
		}

		pmpro_ShowMonthOrYear();

		//draw the chart
		google.charts.load('current', {'packages':['corechart']});
		google.charts.setOnLoadCallback(drawVisualization);
		function drawVisualization() {

			var data = google.visualization.arrayToDataTable([
				<?php
				// Get the label for the "cancellations" index.
				if ( $type === 'signup_v_cancel' ) {
					$cancellations_label = __( 'Cancellations', 'paid-memberships-pro' );
				} elseif ( $type === 'signup_v_expiration' ) {
					$cancellations_label = __( 'Expirations', 'paid-memberships-pro' );
				} else {
					$cancellations_label = __( 'All Cancellations', 'paid-memberships-pro' );
				}
				?>
				['<?php echo esc_html( $date_function ); ?>', '<?php echo esc_html( __( 'Signups', 'paid-memberships-pro' ) ) ?>', '<?php echo esc_html( $cancellations_label ); ?>'],
				<?php foreach ( $dates as $key => $value ) { ?>
				['<?php
					if ( $period == 'monthly' ) {
						echo esc_html( date_i18n( 'M', mktime( 0, 0, 0, $value->date, 2 ) ) );
					} elseif ( $period == 'daily' ) {
						echo esc_html( $key );
					} else {
						echo esc_html( $value->date );
					}
					?>
				', <?php echo esc_html( $value->signups ); ?>, <?php echo esc_html( $value->cancellations ); ?>],
				<?php } ?>
			]);

			var options = {
				colors: ['#0099c6', '#dc3912'],
				chartArea: {width: '90%'},
				legend: {
					alignment: 'center',
					position: 'top',
					textStyle: {color: '#555555', fontSize: '12', italic: false}
				},
				hAxis: {
					title: '<?php echo esc_html( $date_function ); ?>',
					textStyle: {color: '#555555', fontSize: '12', italic: false},
					titleTextStyle: {color: '#555555', fontSize: '20', bold: true, italic: false},
					maxAlternation: 1
				},
				vAxis: {
					format: '0',
					textStyle: {color: '#555555', fontSize: '12', italic: false},
				},
				seriesType: 'bars',
			};

			<?php if ( $type === 'signup_v_cancel' || $type === 'signup_v_expiration' || $type === 'signup_v_all' ) : // Signups vs. cancellations ?>
				var chart = new google.visualization.ColumnChart(document.getElementById('chart_div'));
			<?php endif; ?>
			chart.draw(data, options);
		}
	</script>

	</form>
	<?php
	// Show a table with all of the raw data.
	?>
	<div class="pmpro_table_area">
		<table class="widefat striped">
			<thead>
				<tr>
					<th scope="col"><?php esc_html_e( 'Date', 'paid-memberships-pro' ); ?></th>
					<th scope="col"><?php esc_html_e( 'Signups', 'paid-memberships-pro' ); ?></th>
					<th scope="col"><?php echo esc_html( $cancellations_label ); ?></th>
				</tr>
			</thead>
			<tbody>
				<?php
					foreach ( $dates as $key => $value ) {
					?>
					<tr>
						<th scope="row"><?php
							if ( $period == 'monthly' ) {
								echo esc_html( date_i18n( 'F Y', mktime( 0, 0, 0, $value->date, 1, $year ) ) );
							} elseif ( $period == 'daily' ) {
								echo esc_html( date_i18n( get_option( 'date_format' ), mktime( 0, 0, 0, $month, $key, $year ) ) );
							} else {
								echo esc_html( $value->date );
							}
							?></th>
						<td><?php echo esc_html( $value->signups ); ?></td>
						<td><?php echo esc_html( $value->cancellations ); ?></td>
					</tr>
					<?php
				}
				?>
			</tbody>
			<tfoot>
				<tr>
					<th scope="row"><?php esc_html_e( 'Total', 'paid-memberships-pro' ); ?></th>
					<th><?php echo esc_html( array_sum( wp_list_pluck( $dates, 'signups' ) ) ); ?></th>
					<th><?php echo esc_html( array_sum( wp_list_pluck( $dates, 'cancellations' ) ) ); ?></th>
				</tr>
			</tfoot>
		</table>
	</div>
	<?php
}

function pmpro_getSignups( $period = false, $levels = 'all' ) {
	global $wpdb;

	// Normalize period.
	if ( empty( $period ) ) {
		$period = 'all time';
	}

	$valid_periods = array( 'today', 'this month', 'this year', 'all time' );
	if ( ! in_array( $period, $valid_periods, true ) ) {
		$period = 'all time';
	}

	// Disallow multiple levels. Allow: 'all' or a single level ID.
	if ( is_array( $levels ) ) {
		$clean = array_values( array_filter( array_map( 'intval', $levels ) ) );
		if ( count( $clean ) > 1 ) {
			_doing_it_wrong(
				__FUNCTION__,
				__( 'The $levels parameter only supports a single level ID or "all". Passing multiple level IDs is no longer supported.', 'paid-memberships-pro' ),
				'3.7'
			);
			return 0;
		}
		$levels = ! empty( $clean ) ? $clean[0] : 'all';
	} elseif ( is_string( $levels ) && strpos( $levels, ',' ) !== false ) {
		$clean = array_values( array_filter( array_map( 'intval', explode( ',', $levels ) ) ) );
		if ( count( $clean ) > 1 ) {
			_doing_it_wrong(
				__FUNCTION__,
				__( 'The $levels parameter only supports a single level ID or "all". Passing multiple level IDs is no longer supported.', 'paid-memberships-pro' ),
				'3.7'
			);
			return 0;
		}
		$levels = ! empty( $clean ) ? $clean[0] : 'all';
	}

	// Request-level cache to avoid repeated get_transient calls.
	static $cache = null;
	static $cache_loaded = false;

	$transient_key = 'pmpro_report_memberships_signups_v2';

	if ( ! $cache_loaded ) {
		$cache = get_transient( $transient_key );
		if ( ! is_array( $cache ) ) {
			$cache = array();
		}
		$cache_loaded = true;
	}

	// Compute boundary timestamps for each period (WP timezone).
	$now = current_time( 'timestamp' );

	$bounds = array(
		'today'      => date_i18n( 'Y-m-d 00:00:00', $now ),
		'this month' => date_i18n( 'Y-m-01 00:00:00', $now ),
		'this year'  => date_i18n( 'Y-01-01 00:00:00', $now ),
		'all time'   => '1970-01-01 00:00:00',
	);

	// Rebuild cache if missing OR if boundaries changed since it was built.
	$needs_rebuild = empty( $cache['_primed'] )
		|| empty( $cache['_bounds'] )
		|| $cache['_bounds']['today'] !== $bounds['today']
		|| $cache['_bounds']['this month'] !== $bounds['this month']
		|| $cache['_bounds']['this year'] !== $bounds['this year'];

	if ( $needs_rebuild ) {
		$sql = $wpdb->prepare(
			"
			SELECT mu.membership_id,
				COUNT(DISTINCT CASE WHEN mu.startdate >= %s THEN mu.user_id END) AS today,
				COUNT(DISTINCT CASE WHEN mu.startdate >= %s THEN mu.user_id END) AS this_month,
				COUNT(DISTINCT CASE WHEN mu.startdate >= %s THEN mu.user_id END) AS this_year,
				COUNT(DISTINCT mu.user_id) AS all_time
			FROM {$wpdb->pmpro_memberships_users} mu
			WHERE mu.startdate >= %s
			GROUP BY mu.membership_id WITH ROLLUP
			",
			$bounds['today'],
			$bounds['this month'],
			$bounds['this year'],
			$bounds['all time']
		);

		$rows = $wpdb->get_results( $sql );

		$cache = array(
			'_primed' => true,
			'_bounds' => $bounds,
			'today'      => array( 'all' => 0, 'levels' => array() ),
			'this month' => array( 'all' => 0, 'levels' => array() ),
			'this year'  => array( 'all' => 0, 'levels' => array() ),
			'all time'   => array( 'all' => 0, 'levels' => array() ),
		);

		foreach ( (array) $rows as $row ) {
			$counts = array(
				'today'      => (int) $row->today,
				'this month' => (int) $row->this_month,
				'this year'  => (int) $row->this_year,
				'all time'   => (int) $row->all_time,
			);

			if ( is_null( $row->membership_id ) ) {
				// ROLLUP total row = all levels.
				foreach ( $counts as $p => $count ) {
					$cache[ $p ]['all'] = $count;
				}
			} else {
				$level_id = (int) $row->membership_id;
				foreach ( $counts as $p => $count ) {
					$cache[ $p ]['levels'][ $level_id ] = $count;
				}
			}
		}

		set_transient( $transient_key, $cache, DAY_IN_SECONDS );
	}

	// Serve "all" quickly.
	if ( empty( $levels ) || $levels === 'all' ) {
		return (int) $cache[ $period ]['all'];
	}

	$level_id = (int) $levels;
	if ( $level_id <= 0 ) {
		return 0;
	}

	return isset( $cache[ $period ]['levels'][ $level_id ] ) ? (int) $cache[ $period ]['levels'][ $level_id ] : 0;
}

/**
 * get cancellations by status
 *
 * @param string            $period - Either a string description ('today', 'this month', 'this year')
 * @param array(int)|string $levels - Either an array of level IDs or the string 'all'
 * @param array(string)     $status - Array of statuses to fetch data for
 * @return null|int - The # of cancellations for the period specified
 */
function pmpro_getCancellations( $period = null, $levels = 'all', $status = array( 'inactive', 'expired', 'cancelled', 'admin_cancelled' ) ) {
	// make sure status is an array
	if ( ! is_array( $status ) ) {
		$status = array( $status );
	}

	// check for a transient
	$cache = get_transient( 'pmpro_report_memberships_cancellations' );
	$hash  = md5(
		$period .
		implode( ',', is_array( $levels ) ? $levels : array( $levels ) ) .
		implode( ',', $status )
	);

	if ( ! empty( $cache ) && isset( $cache[ $hash ] ) ) {
		return $cache[ $hash ];
	}

	// figure out start date
	$now  = current_time( 'timestamp' );
	$year = date_i18n( 'Y', $now );

	if ( $period == 'today' ) {
		$startdate = date_i18n( 'Y-m-d', $now ) . ' 00:00:00';
		$enddate   = "'" . date_i18n( 'Y-m-d', $now ) . " 23:59:59'";
	} elseif ( $period == 'this month' ) {
		$startdate = date_i18n( 'Y-m', $now ) . '-01 00:00:00';
		$enddate   = "CONCAT(LAST_DAY('" . date_i18n( 'Y-m', $now ) . "-01'), ' 23:59:59')";
	} elseif ( $period == 'this year' ) {
		$startdate = date_i18n( 'Y', $now ) . '-01-01 00:00:00';
		$enddate   = "'" . date_i18n( 'Y', $now ) . "-12-" . date_i18n( 't', strtotime( $startdate ) ) . " 23:59:59'";
	} else {
		// all time
		$startdate = '1970-01-01';  // all time (no point in using a value prior to the start of the UNIX epoch)
		$enddate   = "'" . strval( intval( $year ) + 1 ) . "-01-01'";
	}

	/*
		build query.
		cancellations are marked in the memberships users table with status 'inactive', 'expired', 'cancelled', 'admin_cancelled'
		we try to ignore cancellations when the user gets a new level with 24 hours (probably an upgrade or downgrade)
	*/
	global $wpdb;

	// Note here that we no longer esc_sql the $startdate and $enddate
	// Escaping broke the MYSQL we passed in.
	// We generated these vars and can trust them.
	$sqlQuery = "
		SELECT COUNT( DISTINCT mu1.user_id )
		FROM {$wpdb->pmpro_memberships_users} AS mu1
		WHERE mu1.status IN('" . implode( "','", array_map( 'esc_sql', $status ) ) . "')
			AND mu1.enddate >= '" . $startdate . "'
			AND mu1.enddate <= " . $enddate . '
		';

	// restrict by level
	if ( ! empty( $levels ) && $levels != 'all' ) {
		// Let's make sure that each ID inside of $levels is an integer.
		if ( ! is_array( $levels ) ) {
			$levels = explode( ',', $levels );
		}
		$levels    = implode( ',', array_map( 'intval', $levels ) );
		$sqlQuery .= 'AND mu1.membership_id IN(' . $levels . ') ';
	}

	/**
	 * Filter query to get cancellation numbers in signups vs cancellations detailed report.
	 *
	 * @since 1.8.8
	 *
	 * @param string $sqlQuery The current SQL
	 * @param string $period Period for report. today, this month, this year, empty string for all time.
	 * @param array(int) $levels Level IDs to include in report.
	 * @param array(string) $status Statuses to include as cancelled.
	 */
	$sqlQuery = apply_filters( 'pmpro_reports_get_cancellations_sql', $sqlQuery, $period, $levels, $status );

	$cancellations = $wpdb->get_var( $sqlQuery );

	// save in cache
	if ( ! empty( $cache ) && ! empty( $cache[ $hash ] ) ) {
		$cache[ $hash ] = $cancellations;
	} elseif ( ! empty( $cache ) ) {
		$cache[ $hash ] = $cancellations;
	} else {
		$cache = array( $hash => $cancellations );
	}

	set_transient( 'pmpro_report_memberships_cancellations', $cache, 3600 * 24 );

	return $cancellations;
}

// get Cancellation Rate
function pmpro_getCancellationRate( $period, $levels = 'all', $status = null ) {
	// make sure status is an array
	if ( ! is_array( $status ) ) {
		$status = array( $status );
	}

	// check for a transient
	$cache = get_transient( 'pmpro_report_cancellation_rate' );
	$hash  = md5( $period . $levels . implode( '', $status ) );
	if ( ! empty( $cache ) && isset( $cache[ $hash ] ) ) {
		return $cache[ $hash ];
	}

	$signups       = pmpro_getSignups( $period, $levels );
	$cancellations = pmpro_getCancellations( $period, $levels, $status );

	if ( empty( $signups ) ) {
		return false;
	}

	$rate = number_format( ( $cancellations / $signups ) * 100, 2 );

	// save in cache
	if ( ! empty( $cache ) && ! empty( $cache[ $period ] ) ) {
		$cache[ $period ][ $levels ] = $rate;
	} elseif ( ! empty( $cache ) ) {
		$cache[ $period ] = array( $levels => $rate );
	} else {
		$cache = array( $period => array( $levels => $rate ) );
	}

	set_transient( 'pmpro_report_cancellation_rate', $cache, 3600 * 24 );

	return $rate;
}

// delete transients when an order goes through
function pmpro_report_memberships_delete_transients() {
	delete_transient( 'pmpro_report_cancellation_rate' );
	delete_transient( 'pmpro_report_memberships_cancellations' );
	delete_transient( 'pmpro_report_memberships_signups' );
	delete_transient( 'pmpro_report_memberships_signups_v2' );
}
add_action( 'pmpro_updated_order', 'pmpro_report_memberships_delete_transients' );
add_action( 'pmpro_after_checkout', 'pmpro_report_memberships_delete_transients' );
add_action( 'pmpro_after_change_membership_level', 'pmpro_report_memberships_delete_transients' );


/**
 * Creates an array of membership level ID's for querying.
 *
 * @param $type string type of membership level you want to retrieve "free" or "paid".
 * @since 2.0
 */
function pmpro_report_get_levels( $type = null ) {

	if ( empty( $type ) ) {
		return;
	}

	$level_data = pmpro_getAllLevels( true, true );
	$r          = array();

	foreach ( $level_data as $key => $value ) {
		if ( $type === 'free' && pmpro_isLevelFree( $value ) ) {
			$r[] = intval( $value->id );
		} elseif ( $type === 'paid' && ! pmpro_isLevelFree( $value ) ) {
			$r[] = intval( $value->id );
		}
	}

	// implode it before returning it.
	$r = implode( ',', $r );

	return $r;
}
