<?php

namespace Automattic\WooCommerce\Admin\Features\MarketingRecommendations;

use Automattic\WooCommerce\Admin\RemoteSpecs\RemoteSpecsEngine;

defined( 'ABSPATH' ) || exit;

/**
 * Marketing Recommendations engine.
 * This goes through the specs and gets marketing recommendations.
 */
class Init extends RemoteSpecsEngine {
	/**
	 * Slug of the category specifying marketing extensions on the WooCommerce.com store.
	 *
	 * @var string
	 */
	const MARKETING_EXTENSION_CATEGORY_SLUG = 'marketing';

	/**
	 * Slug of the subcategory specifying marketing channels on the WooCommerce.com store.
	 *
	 * @var string
	 */
	const MARKETING_CHANNEL_SUBCATEGORY_SLUG = 'sales-channels';

	/**
	 * Constructor.
	 */
	public function __construct() {
		add_action( 'woocommerce_updated', array( __CLASS__, 'delete_specs_transient' ) );
	}

	/**
	 * Delete the specs transient.
	 */
	public static function delete_specs_transient() {
		MarketingRecommendationsDataSourcePoller::get_instance()->delete_specs_transient();
		MiscRecommendationsDataSourcePoller::get_instance()->delete_specs_transient();
	}

	/**
	 * Get specs or fetch remotely if they don't exist.
	 */
	public static function get_specs() {
		if ( 'no' === get_option( 'woocommerce_show_marketplace_suggestions', 'yes' ) ) {
			return DefaultMarketingRecommendations::get_all();
		}
		$specs = MarketingRecommendationsDataSourcePoller::get_instance()->get_specs_from_data_sources();

		// Fetch specs if they don't yet exist.
		if ( ! is_array( $specs ) || 0 === count( $specs ) ) {
			return DefaultMarketingRecommendations::get_all();
		}

		return $specs;
	}

	/**
	 * Get misc recommendations specs or fetch remotely if they don't exist.
	 *
	 * @since 9.5.0
	 */
	public static function get_misc_recommendations_specs() {
		if ( 'no' === get_option( 'woocommerce_show_marketplace_suggestions', 'yes' ) ) {
			return array();
		}
		$specs = MiscRecommendationsDataSourcePoller::get_instance()->get_specs_from_data_sources();

		// Return empty specs if they don't yet exist.
		if ( ! is_array( $specs ) ) {
			return array();
		}

		return $specs;
	}

	/**
	 * Process specs.
	 *
	 * @param array|null $specs Marketing recommendations spec array.
	 * @return array
	 */
	protected static function evaluate_specs( array $specs = null ) {
		$suggestions = array();
		$errors      = array();

		foreach ( $specs as $spec ) {
			try {
				$suggestions[] = self::object_to_array( $spec );
			} catch ( \Throwable $e ) {
				$errors[] = $e;
			}
		}

		return array(
			'suggestions' => $suggestions,
			'errors'      => $errors,
		);
	}

	/**
	 * Load recommended plugins from WooCommerce.com
	 *
	 * @return array
	 */
	public static function get_recommended_plugins(): array {
		$specs   = self::get_specs();
		$results = self::evaluate_specs( $specs );

		$specs_to_return = $results['suggestions'];
		$specs_to_save   = null;

		if ( empty( $specs_to_return ) ) {
			// When suggestions is empty, replace it with defaults and save for 3 hours.
			$specs_to_save   = DefaultMarketingRecommendations::get_all();
			$specs_to_return = self::evaluate_specs( $specs_to_save )['suggestions'];
		} elseif ( count( $results['errors'] ) > 0 ) {
			// When suggestions is not empty but has errors, save it for 3 hours.
			$specs_to_save = $specs;
		}

		if ( $specs_to_save ) {
			MarketingRecommendationsDataSourcePoller::get_instance()->set_specs_transient( $specs_to_save, 3 * HOUR_IN_SECONDS );
		}
		$errors = $results['errors'];
		if ( ! empty( $errors ) ) {
			self::log_errors( $errors );
		}

		return $specs_to_return;
	}

	/**
	 * Return only the recommended marketing channels from WooCommerce.com.
	 *
	 * @return array
	 */
	public static function get_recommended_marketing_channels(): array {
		return array_filter(
			self::get_recommended_plugins(),
			function ( array $plugin_data ) {
				return self::is_marketing_channel_plugin( $plugin_data );
			}
		);
	}

	/**
	 * Return all recommended marketing extensions EXCEPT the marketing channels from WooCommerce.com.
	 *
	 * @return array
	 */
	public static function get_recommended_marketing_extensions_excluding_channels(): array {
		return array_filter(
			self::get_recommended_plugins(),
			function ( array $plugin_data ) {
				return self::is_marketing_plugin( $plugin_data ) && ! self::is_marketing_channel_plugin( $plugin_data );
			}
		);
	}

	/**
	 * Load misc recommendations from WooCommerce.com
	 *
	 * @since 9.5.0
	 * @return array
	 */
	public static function get_misc_recommendations(): array {
		$specs   = self::get_misc_recommendations_specs();
		$results = self::evaluate_specs( $specs );

		$specs_to_return = $results['suggestions'];
		$specs_to_save   = null;

		if ( empty( $specs_to_return ) ) {
			// When misc_recommendations is empty, replace it with defaults and save for 3 hours.
			$specs_to_save = array();
		} elseif ( count( $results['errors'] ) > 0 ) {
			// When misc_recommendations is not empty but has errors, save it for 3 hours.
			$specs_to_save = $specs;
		}

		if ( $specs_to_save ) {
			MiscRecommendationsDataSourcePoller::get_instance()->set_specs_transient( $specs_to_save, 3 * HOUR_IN_SECONDS );
		}
		$errors = $results['errors'];
		if ( ! empty( $errors ) ) {
			self::log_errors( $errors );
		}

		return $specs_to_return;
	}

	/**
	 * Returns whether a plugin is a marketing extension.
	 *
	 * @param array $plugin_data The plugin properties returned by the API.
	 *
	 * @return bool
	 */
	protected static function is_marketing_plugin( array $plugin_data ): bool {
		$categories = $plugin_data['categories'] ?? array();

		return in_array( self::MARKETING_EXTENSION_CATEGORY_SLUG, $categories, true );
	}

	/**
	 * Returns whether a plugin is a marketing channel.
	 *
	 * @param array $plugin_data The plugin properties returned by the API.
	 *
	 * @return bool
	 */
	protected static function is_marketing_channel_plugin( array $plugin_data ): bool {
		if ( ! self::is_marketing_plugin( $plugin_data ) ) {
			return false;
		}

		$subcategories = $plugin_data['subcategories'] ?? array();
		foreach ( $subcategories as $subcategory ) {
			if ( isset( $subcategory['slug'] ) && self::MARKETING_CHANNEL_SUBCATEGORY_SLUG === $subcategory['slug'] ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Convert an object to an array.
	 * This is used to convert the specs to an array so that they can be returned by the API.
	 *
	 * @param mixed $obj Object to convert.
	 * @param array &$visited Reference to an array keeping track of all seen objects to detect circular references.
	 * @return array
	 */
	public static function object_to_array( $obj, &$visited = array() ) {
		if ( is_object( $obj ) ) {
			if ( in_array( $obj, $visited, true ) ) {
				// Circular reference detected.
				return null;
			}
			$visited[] = $obj;
			$obj       = (array) $obj;
		}
		if ( is_array( $obj ) ) {
			$new = array();
			foreach ( $obj as $key => $val ) {
				$new[ $key ] = self::object_to_array( $val, $visited );
			}
		} else {
			$new = $obj;
		}
		return $new;
	}
}
