<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\API\Google;

use Automattic\WooCommerce\Admin\API\Reports\TimeInterval;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Google\Query\AdsCampaignReportQuery;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Google\Query\AdsProductReportQuery;
use Automattic\WooCommerce\GoogleListingsAndAds\API\MicroTrait;
use Automattic\WooCommerce\GoogleListingsAndAds\Exception\ExceptionWithResponseData;
use Automattic\WooCommerce\GoogleListingsAndAds\Exception\InvalidValue;
use Automattic\WooCommerce\GoogleListingsAndAds\Google\Ads\GoogleAdsClient;
use Automattic\WooCommerce\GoogleListingsAndAds\Internal\ContainerAwareTrait;
use Automattic\WooCommerce\GoogleListingsAndAds\Internal\Interfaces\ContainerAwareInterface;
use Automattic\WooCommerce\GoogleListingsAndAds\Options\OptionsAwareInterface;
use Automattic\WooCommerce\GoogleListingsAndAds\Options\OptionsAwareTrait;
use Google\ApiCore\ApiException;
use Google\Ads\GoogleAds\V23\Common\Segments;
use Google\Ads\GoogleAds\V23\Services\GoogleAdsRow;
use DateTime;
use DateTimeInterface;

/**
 * Class AdsReport
 *
 * ContainerAware used for:
 * - AdsCampaign
 *
 * @package Automattic\WooCommerce\GoogleListingsAndAds\API\Google
 */
class AdsReport implements ContainerAwareInterface, OptionsAwareInterface {

	use ContainerAwareTrait;
	use ExceptionTrait;
	use MicroTrait;
	use OptionsAwareTrait;
	use ReportTrait;

	/**
	 * The Google Ads Client.
	 *
	 * @var GoogleAdsClient
	 */
	protected $client;

	/**
	 * Have we completed the conversion to PMax campaigns.
	 *
	 * @var bool
	 */
	protected $has_converted;

	/**
	 * AdsReport constructor.
	 *
	 * @param GoogleAdsClient $client
	 */
	public function __construct( GoogleAdsClient $client ) {
		$this->client = $client;
	}

	/**
	 * Get report data for campaigns.
	 *
	 * @param string $type Report type (campaigns or products).
	 * @param array  $args Query arguments.
	 *
	 * @return array
	 * @throws ExceptionWithResponseData If the report data can't be retrieved.
	 */
	public function get_report_data( string $type, array $args ): array {
		$ads_id       = (string) $this->options->get_ads_id();
		$cache_key    = 'gla_ads_report_' . $type . '_' . md5(
			wp_json_encode(
				[
					'ads_id' => $ads_id,
					'args'   => $this->normalize_args_for_cache( $args ),
				]
			)
		);
		$cached_value = get_transient( $cache_key );

		if ( is_array( $cached_value ) ) {
			return $cached_value;
		}

		if ( false !== $cached_value ) {
			delete_transient( $cache_key );
		}
		try {
			$this->has_converted = 'converted' === $this->container->get( AdsCampaign::class )->get_campaign_convert_status();

			if ( 'products' === $type ) {
				$query = new AdsProductReportQuery( $args );
			} else {
				$query = new AdsCampaignReportQuery( $args );
			}

			$results = $query
				->set_client( $this->client, $this->options->get_ads_id() )
				->get_results();
			$page    = $results->getPage();

			$this->init_report_totals( $args['fields'] ?? [] );

			// Iterate only this page (iterateAllElements will iterate all pages).
			foreach ( $page->getIterator() as $row ) {
				$this->add_report_row( $type, $row, $args );
			}

			if ( $page->hasNextPage() ) {
				$this->report_data['next_page'] = $page->getNextPageToken();
			}

			// Sort intervals to generate an ordered graph.
			if ( isset( $this->report_data['intervals'] ) ) {
				ksort( $this->report_data['intervals'] );
			}

			$this->remove_report_indexes( [ 'products', 'campaigns', 'intervals' ] );

			set_transient( $cache_key, $this->report_data, HOUR_IN_SECONDS );

			return $this->report_data;
		} catch ( ApiException $e ) {
			do_action( 'woocommerce_gla_ads_client_exception', $e, __METHOD__ );

			$errors = $this->get_exception_errors( $e );
			throw new ExceptionWithResponseData(
				/* translators: %s Error message */
				sprintf( __( 'Unable to retrieve report data: %s', 'google-listings-and-ads' ), reset( $errors ) ),
				$this->map_grpc_code_to_http_status_code( $e ),
				null,
				[
					'errors'            => $errors,
					'report_type'       => $type,
					'report_query_args' => $args,
				]
			);
		}
	}

	/**
	 * Normalize query args for stable cache key generation.
	 *
	 * Converts DateTime values to 'Y-m-d' strings (matching how ReportQueryTrait
	 * formats them for the actual query) and applies a recursive ksort so that
	 * argument key ordering does not affect the cache key.
	 *
	 * @param array $args Raw query arguments.
	 * @return array Normalized args safe for hashing.
	 */
	private function normalize_args_for_cache( array $args ): array {
		array_walk_recursive(
			$args,
			function ( &$value ) {
				if ( $value instanceof DateTimeInterface ) {
					$value = $value->format( 'Y-m-d' );
				}
			}
		);

		$sort_recursive = function ( array &$arr ) use ( &$sort_recursive ) {
			ksort( $arr );
			foreach ( $arr as &$value ) {
				if ( is_array( $value ) ) {
					$sort_recursive( $value );
				}
			}
		};
		$sort_recursive( $args );

		return $args;
	}

	/**
	 * Add data for a report row.
	 *
	 * @param string       $type Report type (campaigns or products).
	 * @param GoogleAdsRow $row  Report row.
	 * @param array        $args Request arguments.
	 */
	protected function add_report_row( string $type, GoogleAdsRow $row, array $args ) {
		$campaign = $row->getCampaign();
		$segments = $row->getSegments();
		$metrics  = $this->get_report_row_metrics( $row, $args );

		if ( 'products' === $type && $segments ) {
			$product_id = $segments->getProductItemId();
			$this->increase_report_data(
				'products',
				(string) $product_id,
				[
					'id'        => $product_id,
					'name'      => $segments->getProductTitle(),
					'subtotals' => $metrics,
				]
			);
		}

		if ( 'campaigns' === $type && $campaign ) {
			$campaign_id   = $campaign->getId();
			$campaign_name = $campaign->getName();
			$campaign_type = CampaignType::label( $campaign->getAdvertisingChannelType() );
			$is_converted  = $this->has_converted && CampaignType::PERFORMANCE_MAX !== $campaign_type;

			$this->increase_report_data(
				'campaigns',
				(string) $campaign_id,
				[
					'id'          => $campaign_id,
					'name'        => $campaign_name,
					'status'      => CampaignStatus::label( $campaign->getStatus() ),
					'isConverted' => $is_converted,
					'subtotals'   => $metrics,
				]
			);
		}

		if ( $segments && ! empty( $args['interval'] ) ) {
			$interval = $this->get_segment_interval( $args['interval'], $segments );

			$this->increase_report_data(
				'intervals',
				$interval,
				[
					'interval'  => $interval,
					'subtotals' => $metrics,
				]
			);
		}

		$this->increase_report_totals( $metrics );
	}

	/**
	 * Get metrics for a report row.
	 *
	 * @param GoogleAdsRow $row  Report row.
	 * @param array        $args Request arguments.
	 *
	 * @return array
	 */
	protected function get_report_row_metrics( GoogleAdsRow $row, array $args ): array {
		$metrics = $row->getMetrics();

		if ( ! $metrics || empty( $args['fields'] ) ) {
			return [];
		}

		$data = [];
		foreach ( $args['fields'] as $field ) {
			switch ( $field ) {
				case 'clicks':
					$data['clicks'] = $metrics->getClicks();
					break;
				case 'impressions':
					$data['impressions'] = $metrics->getImpressions();
					break;
				case 'spend':
					$data['spend'] = $this->from_micro( $metrics->getCostMicros() );
					break;
				case 'sales':
					$data['sales'] = $metrics->getConversionsValue();
					break;
				case 'conversions':
					$data['conversions'] = $metrics->getConversions();
					break;
			}
		}

		return $data;
	}

	/**
	 * Get a unique interval index based on the segments data.
	 *
	 * Types:
	 * day     = <year>-<month>-<day>
	 * week    = <year>-<weeknumber>
	 * month   = <year>-<month>
	 * quarter = <year>-<quarter>
	 * year    = <year>
	 *
	 * @param string   $interval Interval type.
	 * @param Segments $segments Report segment data.
	 *
	 * @return string
	 * @throws InvalidValue When invalid interval type is given.
	 */
	protected function get_segment_interval( string $interval, Segments $segments ): string {
		switch ( $interval ) {
			case 'day':
				$date = new DateTime( $segments->getDate() );
				break;
			case 'week':
				$date = new DateTime( $segments->getWeek() );
				break;
			case 'month':
				$date = new DateTime( $segments->getMonth() );
				break;
			case 'quarter':
				$date = new DateTime( $segments->getQuarter() );
				break;
			case 'year':
				$date = DateTime::createFromFormat( 'Y', (string) $segments->getYear() );
				break;
			default:
				throw InvalidValue::not_in_allowed_list( $interval, [ 'day', 'week', 'month', 'quarter', 'year' ] );
		}
		return TimeInterval::time_interval_id( $interval, $date );
	}
}
