<?php
/**
 * Class Google\Site_Kit\Modules\Analytics_4\Report\Response
 *
 * @package   Google\Site_Kit\Modules\Analytics_4\Report
 * @copyright 2023 Google LLC
 * @license   https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
 * @link      https://sitekit.withgoogle.com
 */

namespace Google\Site_Kit\Modules\Analytics_4\Report;

use Google\Site_Kit\Core\REST_API\Data_Request;
use Google\Site_Kit\Modules\Analytics_4\Report;
use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\DateRange as Google_Service_AnalyticsData_DateRange;
use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\Row as Google_Service_AnalyticsData_Row;
use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\RunReportResponse as Google_Service_AnalyticsData_RunReportResponse;

/**
 * Class for Analytics 4 report responses.
 *
 * @since 1.99.0
 * @access private
 * @ignore
 */
class Response extends Report {

	use Row_Trait;

	/**
	 * Parses the report response, and pads the report data with zero-data rows where rows are missing. This only applies for reports which request a single `date` dimension.
	 *
	 * @since 1.99.0
	 *
	 * @param Data_Request                                   $data     Data request object.
	 * @param Google_Service_AnalyticsData_RunReportResponse $response Request response.
	 * @return mixed Parsed response data on success, or WP_Error on failure.
	 */
	public function parse_response( Data_Request $data, $response ) {
		// Return early if the response is not of the expected type.
		if ( ! $response instanceof Google_Service_AnalyticsData_RunReportResponse ) {
			return $response;
		}

		// Get report dimensions and return early if there is either more than one dimension or
		// the only dimension is not "date".
		$dimensions = $this->parse_dimensions( $data );
		if ( count( $dimensions ) !== 1 || $dimensions[0]->getName() !== 'date' ) {
			return $response;
		}

		// Get date ranges and return early if there are no date ranges for this report.
		$date_ranges = $this->get_sorted_dateranges( $data );
		if ( empty( $date_ranges ) ) {
			return $response;
		}

		// Get all available dates in the report.
		$existing_rows = array();
		foreach ( $response->getRows() as $row ) {
			$dimension_values = $row->getDimensionValues();

			$range = 'date_range_0';
			if ( count( $dimension_values ) > 1 ) {
				// Considering this code will only be run when we are requesting a single dimension, `date`,
				// the implication is that the row will _only_ have an additional dimension when multiple
				// date ranges are requested.
				//
				// In this scenario, the dimension at index 1 will have a value of `date_range_{i}`, where
				// `i` is the zero-based index of the date range.
				$range = $dimension_values[1]->getValue();
			}

			$range = str_replace( 'date_range_', '', $range );
			$date  = $dimension_values[0]->getValue();
			$key   = self::get_response_row_key( $date, is_numeric( $range ) ? $range : false );

			$existing_rows[ $key ] = $row;
		}

		$metric_headers  = $response->getMetricHeaders();
		$ranges_count    = count( $date_ranges );
		$multiple_ranges = $ranges_count > 1;
		$rows            = array();

		// Add rows for the current date for each date range.
		self::iterate_date_ranges(
			$date_ranges,
			function ( $date ) use ( &$rows, $existing_rows, $date_ranges, $ranges_count, $metric_headers, $multiple_ranges ) {
				for ( $i = 0; $i < $ranges_count; $i++ ) {
					$date_range_name = $date_ranges[ $i ]->getName();
					if ( empty( $date_range_name ) ) {
						$date_range_name = $i;
					}

					// Copy the existing row if it is available, otherwise create a new zero-value row.
					$key          = self::get_response_row_key( $date, $i );
					$rows[ $key ] = isset( $existing_rows[ $key ] )
						? $existing_rows[ $key ]
						: $this->create_report_row( $metric_headers, $date, $multiple_ranges ? $date_range_name : false );
				}
			}
		);

		// If we have the same number of rows as in the response at the moment, then
		// we can return the response without setting the new rows back into the response.
		$new_rows_count = count( $rows );
		if ( $new_rows_count <= $response->getRowCount() ) {
			return $response;
		}

		// If we have multiple date ranges, we need to sort rows to have them in
		// the correct order.
		if ( $multiple_ranges ) {
			$rows = self::sort_response_rows( $rows, $date_ranges );
		}

		// Set updated rows back to the response object.
		$response->setRows( array_values( $rows ) );
		$response->setRowCount( $new_rows_count );

		return $response;
	}

	/**
	 * Gets the response row key composed from the date and the date range index values.
	 *
	 * @since 1.99.0
	 *
	 * @param string   $date             The date of the row to return key for.
	 * @param int|bool $date_range_index The date range index, or FALSE if no index is available.
	 * @return string The row key.
	 */
	protected static function get_response_row_key( $date, $date_range_index ) {
		return "{$date}_{$date_range_index}";
	}

	/**
	 * Returns sorted and filtered date ranges received in the request params. All corrupted date ranges
	 * are ignored and not included in the returning list.
	 *
	 * @since 1.99.0
	 *
	 * @param Data_Request $data Data request object.
	 * @return Google_Service_AnalyticsData_DateRange[] An array of AnalyticsData DateRange objects.
	 */
	protected function get_sorted_dateranges( Data_Request $data ) {
		$date_ranges = $this->parse_dateranges( $data );
		if ( empty( $date_ranges ) ) {
			return $date_ranges;
		}

		// Filter out all corrupted date ranges.
		$date_ranges = array_filter(
			$date_ranges,
			function ( $range ) {
				$start = strtotime( $range->getStartDate() );
				$end   = strtotime( $range->getEndDate() );
				return ! empty( $start ) && ! empty( $end );
			}
		);

		// Sort date ranges preserving keys to have the oldest date range at the beginning and
		// the latest date range at the end.
		uasort(
			$date_ranges,
			function ( $a, $b ) {
				$a_start = strtotime( $a->getStartDate() );
				$b_start = strtotime( $b->getStartDate() );
				return $a_start - $b_start;
			}
		);

		return $date_ranges;
	}

	/**
	 * Sorts response rows using the algorithm similar to the one that Analytics 4 uses internally
	 * and returns sorted rows.
	 *
	 * @since 1.99.0
	 *
	 * @param Google_Service_AnalyticsData_Row[]       $rows        The current report rows.
	 * @param Google_Service_AnalyticsData_DateRange[] $date_ranges The report date ranges.
	 * @return Google_Service_AnalyticsData_Row[] Sorted rows.
	 */
	protected static function sort_response_rows( $rows, $date_ranges ) {
		$sorted_rows  = array();
		$ranges_count = count( $date_ranges );

		self::iterate_date_ranges(
			$date_ranges,
			function ( $date, $range_index ) use ( &$sorted_rows, $ranges_count, $rows ) {
				// First take the main date range row.
				$key                 = self::get_response_row_key( $date, $range_index );
				$sorted_rows[ $key ] = $rows[ $key ];

				// Then take all remaining rows.
				for ( $i = 0; $i < $ranges_count; $i++ ) {
					if ( $i !== $range_index ) {
						$key                 = self::get_response_row_key( $date, $i );
						$sorted_rows[ $key ] = $rows[ $key ];
					}
				}
			}
		);

		return $sorted_rows;
	}

	/**
	 * Iterates over the date ranges and calls callback for each date in each range.
	 *
	 * @since 1.99.0
	 *
	 * @param Google_Service_AnalyticsData_DateRange[] $date_ranges The report date ranges.
	 * @param callable                                 $callback    The callback to execute for each date.
	 */
	protected static function iterate_date_ranges( $date_ranges, $callback ) {
		foreach ( $date_ranges as $date_range_index => $date_range ) {
			$now = strtotime( $date_range->getStartDate() );
			$end = strtotime( $date_range->getEndDate() );

			do {
				call_user_func(
					$callback,
					gmdate( 'Ymd', $now ),
					$date_range_index
				);

				$now += DAY_IN_SECONDS;
			} while ( $now <= $end );
		}
	}
}
