<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\API\Google;

use Automattic\WooCommerce\GoogleListingsAndAds\API\Google\LocationIDTrait;
use Automattic\WooCommerce\GoogleListingsAndAds\API\MicroTrait;
use Automattic\WooCommerce\GoogleListingsAndAds\Google\Ads\GoogleAdsClient;
use Automattic\WooCommerce\GoogleListingsAndAds\Options\OptionsAwareInterface;
use Automattic\WooCommerce\GoogleListingsAndAds\Options\OptionsAwareTrait;
use Automattic\WooCommerce\GoogleListingsAndAds\Options\TransientsAwareInterface;
use Automattic\WooCommerce\GoogleListingsAndAds\Options\TransientsAwareTrait;
use Automattic\WooCommerce\GoogleListingsAndAds\Options\TransientsInterface;
use Automattic\WooCommerce\GoogleListingsAndAds\PluginHelper;
use Google\Ads\GoogleAds\V23\Enums\AdvertisingChannelTypeEnum\AdvertisingChannelType;
use Google\Ads\GoogleAds\V23\Enums\BiddingStrategyTypeEnum\BiddingStrategyType;
use Google\Ads\GoogleAds\V23\Enums\RecommendationTypeEnum\RecommendationType;
use Google\Ads\GoogleAds\V23\Resources\Recommendation\CampaignBudgetRecommendation;
use Google\Ads\GoogleAds\V23\Services\GenerateRecommendationsRequest;
use Google\Ads\GoogleAds\V23\Services\GenerateRecommendationsRequest\AssetGroupInfo;
use Google\Ads\GoogleAds\V23\Services\GenerateRecommendationsRequest\BiddingInfo;
use Google\ApiCore\ApiException;

/**
 * Class BudgetRecommendations
 * https://developers.google.com/google-ads/api/rest/reference/rest/v20/Recommendation#CampaignBudgetRecommendation
 *
 * @package Automattic\WooCommerce\GoogleListingsAndAds\API\Google
 */
class BudgetRecommendations implements OptionsAwareInterface, TransientsAwareInterface {

	use MicroTrait;
	use OptionsAwareTrait;
	use PluginHelper;
	use TransientsAwareTrait;
	use LocationIDTrait;

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

	/**
	 * @var MerchantMetrics
	 */
	protected $merchant_metrics;

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

	/**
	 * Fetch budget recommendations (with metrics) from Google Ads API.
	 * This function will only return a single recommendation in the list, with the first country code.
	 *
	 * @param array $country_codes List of countries to include.
	 *
	 * @return array|null List of recommendations (including metrics).
	 */
	public function get_recommendations( array $country_codes ): ?array {
		$is_new_customer = 0 === $this->merchant_metrics->get_campaign_count();
		$cache_key       = strtolower( join( '-', $country_codes ) ) . ( $is_new_customer ? '-new' : '-existing' );
		$transient       = $this->transients->get( TransientsInterface::ADS_BUDGET_RECOMMENDATIONS );

		// Check if we have the budget recommendations cached in the transient.
		if ( $transient && ! empty( $transient[ $cache_key ] ) ) {
			return $transient[ $cache_key ];
		}

		$location_ids = $this->get_location_ids( $country_codes );
		$merchant_id  = $this->options->get_merchant_id();

		$request_data = [
			'customer_id'              => $this->options->get_ads_id(),
			'recommendation_types'     => [ RecommendationType::CAMPAIGN_BUDGET ],
			'advertising_channel_type' => AdvertisingChannelType::PERFORMANCE_MAX,
			'positive_locations_ids'   => array_keys( $location_ids ),
			'country_codes'            => $country_codes,
			'is_new_customer'          => $is_new_customer,
			'bidding_info'             => new BiddingInfo(
				[
					'bidding_strategy_type' => BiddingStrategyType::MAXIMIZE_CONVERSION_VALUE,
				]
			),
			'asset_group_info'         => [
				new AssetGroupInfo(
					[
						'final_url' => $this->get_site_url(),
					],
				),
			],
		];

		// Only include merchant_center_account_id if Merchant Center account is connected.
		if ( $merchant_id > 0 ) {
			$request_data['merchant_center_account_id'] = $merchant_id;
		}

		$request = new GenerateRecommendationsRequest( $request_data );

		try {
			$response = $this->client->getRecommendationServiceClient()->generateRecommendations( $request );

			foreach ( $response->getRecommendations() as $recommendation ) {
				$campaign_budget_recommendation = $recommendation->getCampaignBudgetRecommendation();
				if ( ! $campaign_budget_recommendation ) {
					continue;
				}

				// Parse all the returned recommendations and assign to the first country.
				$recommendations = $this->parse_recommendations( $campaign_budget_recommendation, reset( $country_codes ) );
				$this->transients->set( TransientsInterface::ADS_BUDGET_RECOMMENDATIONS, [ $cache_key => $recommendations ], HOUR_IN_SECONDS * 12 );
				return $recommendations;
			}
		} catch ( ApiException $e ) {
			do_action( 'woocommerce_gla_ads_client_exception', $e, __METHOD__ );
		}

		return null;
	}

	/**
	 * Return all budget recommendations including metrics.
	 *
	 * @param CampaignBudgetRecommendation $recommendation  Collection of recommendation options.
	 * @param string                       $country_code    Primary country code.
	 *
	 * @return array|null Recommendations (including metrics).
	 */
	protected function parse_recommendations( CampaignBudgetRecommendation $recommendation, string $country_code ): ?array {
		// Map all available budget options.
		$options = [];
		foreach ( $recommendation->getBudgetOptions() as $budget_option ) {
			$amount = $this->from_micro( $budget_option->getBudgetAmountMicros() );
			if ( ! $amount ) {
				continue;
			}

			$metrics = $budget_option->getImpact()->getPotentialMetrics();

			$options[ (string) $amount ] = [
				'daily_budget' => $amount,
				'metrics'      => [
					'cost'              => $this->from_micro( $metrics->getCostMicros() ),
					'conversions'       => $metrics->getConversions(),
					'conversions_value' => $metrics->getConversionsValue(),
				],
			];
		}

		// Find closest match based on recommended amount.
		$numbers = array_map( 'floatval', array_keys( $options ) );
		$closest = $this->find_closest( $this->from_micro( $recommendation->getRecommendedBudgetAmountMicros() ), $numbers );

		// Add each option and assign it's level
		$recommendations = [];
		foreach ( $options as $option ) {
			if ( $option['daily_budget'] === $closest ) {
				$level = __( 'Recommended', 'google-listings-and-ads' );
				$index = 0;
			} elseif ( $option['daily_budget'] > $closest ) {
				$level = __( 'High', 'google-listings-and-ads' );
				$index = 1;
			} else {
				$level = __( 'Low', 'google-listings-and-ads' );
				$index = 2;
			}

			$recommendations[ $index ] = array_merge(
				$option,
				[
					'country' => $country_code,
					'level'   => $level,
				]
			);
		}

		if ( empty( $recommendations ) ) {
			return null;
		}

		// Sort recommendations in the order: recommended, high, low.
		ksort( $recommendations );
		return $recommendations;
	}

	/**
	 * Find closest matching number in an array of numbers.
	 *
	 * @param float $number  Number to search for.
	 * @param array $numbers List of numbers to search in.
	 *
	 * @return float|null Closest number found.
	 */
	protected function find_closest( float $number, array $numbers ): ?float {
		if ( empty( $numbers ) ) {
			return null;
		}

		usort(
			$numbers,
			function ( $a, $b ) use ( $number ) {
				return abs( $number - (float) $a ) <=> abs( $number - (float) $b );
			}
		);

		return reset( $numbers );
	}
}
