<?php
/**
 * Class MultiCurrency
 *
 * @package WooCommerce\Payments\MultiCurrency
 */

namespace WCPay\MultiCurrency;

use WC_Payments;
use WC_Payments_Account;
use WC_Payments_Utils;
use WC_Payments_API_Client;
use WC_Payments_Localization_Service;
use WCPay\Exceptions\API_Exception;
use WCPay\Logger;
use WCPay\MultiCurrency\Notes\NoteMultiCurrencyAvailable;

defined( 'ABSPATH' ) || exit;

/**
 * Class that controls Multi-Currency functionality.
 */
class MultiCurrency {

	const CURRENCY_SESSION_KEY            = 'wcpay_currency';
	const CURRENCY_META_KEY               = 'wcpay_currency';
	const CURRENCY_CACHE_OPTION           = 'wcpay_multi_currency_cached_currencies';
	const CURRENCY_RETRIEVAL_ERROR_OPTION = 'wcpay_multi_currency_retrieval_error';
	const FILTER_PREFIX                   = 'wcpay_multi_currency_';

	/**
	 * The plugin's ID.
	 *
	 * @var string
	 */
	public $id = 'wcpay_multi_currency';

	/**
	 * The single instance of the class.
	 *
	 * @var ?MultiCurrency
	 */
	protected static $instance = null;

	/**
	 * Static flag to show if the currencies initialization has been completed
	 *
	 * @var bool
	 */
	protected static $is_initialized = false;

	/**
	 * Compatibility instance.
	 *
	 * @var Compatibility
	 */
	protected $compatibility;

	/**
	 * Geolocation instance.
	 *
	 * @var Geolocation
	 */
	protected $geolocation;

	/**
	 * The Currency Switcher Widget instance.
	 *
	 * @var null|CurrencySwitcherWidget
	 */
	protected $currency_switcher_widget;

	/**
	 * Gutenberg Block implementation of the Currency Switcher Widget instance.
	 *
	 * @var CurrencySwitcherBlock
	 */
	protected $currency_switcher_block;

	/**
	 * Utils instance.
	 *
	 * @var Utils
	 */
	protected $utils;

	/**
	 * FrontendPrices instance.
	 *
	 * @var FrontendPrices
	 */
	protected $frontend_prices;

	/**
	 * FrontendCurrencies instance.
	 *
	 * @var FrontendCurrencies
	 */
	protected $frontend_currencies;

	/**
	 * BackendCurrencies instance.
	 *
	 * @var BackendCurrencies
	 */
	protected $backend_currencies;

	/**
	 * StorefrontIntegration instance.
	 *
	 * @var StorefrontIntegration
	 */
	protected $storefront_integration;

	/**
	 * The available currencies.
	 *
	 * @var Currency[]|null
	 */
	protected $available_currencies;

	/**
	 * The default currency.
	 *
	 * @var Currency|null
	 */
	protected $default_currency;

	/**
	 * The enabled currencies.
	 *
	 * @var Currency[]|null
	 */
	protected $enabled_currencies;

	/**
	 * Client for making requests to the WooCommerce Payments API
	 *
	 * @var WC_Payments_API_Client
	 */
	private $payments_api_client;

	/**
	 * Instance of WC_Payments_Account.
	 *
	 * @var WC_Payments_Account
	 */
	private $payments_account;

	/**
	 * Instance of WC_Payments_Localization_Service.
	 *
	 * @var WC_Payments_Localization_Service
	 */
	private $localization_service;

	/**
	 * Tracking instance.
	 *
	 * @var Tracking
	 */
	protected $tracking;

	/**
	 * Simulation variables array.
	 *
	 * @var array
	 */
	protected $simulation_params = [];

	/**
	 * Main MultiCurrency Instance.
	 *
	 * Ensures only one instance of MultiCurrency is loaded or can be loaded.
	 *
	 * @static
	 * @return MultiCurrency - Main instance.
	 */
	public static function instance() {
		if ( is_null( self::$instance ) ) {
			self::$instance = new self( WC_Payments::get_payments_api_client(), WC_Payments::get_account_service(), WC_Payments::get_localization_service() );
		}
		return self::$instance;
	}

	/**
	 * Class constructor.
	 *
	 * @param WC_Payments_API_Client           $payments_api_client  Payments API client.
	 * @param WC_Payments_Account              $payments_account     Payments Account instance.
	 * @param WC_Payments_Localization_Service $localization_service Localization Service instance.
	 */
	public function __construct( WC_Payments_API_Client $payments_api_client, WC_Payments_Account $payments_account, WC_Payments_Localization_Service $localization_service ) {
		$this->payments_api_client     = $payments_api_client;
		$this->payments_account        = $payments_account;
		$this->localization_service    = $localization_service;
		$this->geolocation             = new Geolocation( $this->localization_service );
		$this->utils                   = new Utils();
		$this->compatibility           = new Compatibility( $this, $this->utils );
		$this->currency_switcher_block = new CurrencySwitcherBlock( $this, $this->compatibility );

		if ( is_admin() ) {
			add_filter( 'woocommerce_get_settings_pages', [ $this, 'init_settings_pages' ] );
			add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_admin_scripts' ] );
			add_action( 'admin_head', [ $this, 'set_client_format_and_rounding_precision' ] );
		}

		add_action( 'init', [ $this, 'init' ] );
		add_action( 'rest_api_init', [ $this, 'init_rest_api' ] );
		add_action( 'widgets_init', [ $this, 'init_widgets' ] );

		$is_frontend_request = ! is_admin() && ! defined( 'DOING_CRON' ) && ! WC()->is_rest_api_request();

		if ( $is_frontend_request ) {
			// Make sure that this runs after the main init function.
			add_action( 'init', [ $this, 'update_selected_currency_by_url' ], 11 );
			add_action( 'init', [ $this, 'update_selected_currency_by_geolocation' ], 12 );
			add_action( 'init', [ $this, 'possible_simulation_activation' ], 13 );
			add_action( 'woocommerce_created_customer', [ $this, 'set_new_customer_currency_meta' ] );
		}
	}

	/**
	 * Called after the WooCommerce session has been initialized. Initialises the available currencies,
	 * default currency and enabled currencies for the Multi-Currency plugin.
	 *
	 * @return void
	 */
	public function init() {
		$store_currency_updated = $this->check_store_currency_for_change();

		// If the store currency has been updated, clear the cache to make sure we fetch fresh rates from the server.
		if ( $store_currency_updated ) {
			$this->clear_cache();
		}

		$this->initialize_available_currencies();
		$this->set_default_currency();
		$this->initialize_enabled_currencies();

		// If the store currency has been updated, we need to update the notice that will display any manual currencies.
		if ( $store_currency_updated ) {
			$this->update_manual_rate_currencies_notice_option();
		}

		new PaymentMethodsCompatibility( $this, WC_Payments::get_gateway() );
		new AdminNotices();
		new UserSettings( $this );
		new Analytics( $this );

		$this->frontend_prices     = new FrontendPrices( $this, $this->compatibility );
		$this->frontend_currencies = new FrontendCurrencies( $this, $this->localization_service, $this->utils, $this->compatibility );
		$this->backend_currencies  = new BackendCurrencies( $this, $this->localization_service );
		$this->tracking            = new Tracking( $this );

		add_action( 'woocommerce_order_refunded', [ $this, 'add_order_meta_on_refund' ], 50, 2 );

		// Check to make sure there are enabled currencies, then for Storefront being active, and then load the integration.
		$theme = wp_get_theme();
		if ( 'storefront' === $theme->get_stylesheet() || 'storefront' === $theme->get_template() ) {
			$this->storefront_integration = new StorefrontIntegration( $this );
		}

		if ( is_admin() ) {
			add_action( 'admin_init', [ __CLASS__, 'add_woo_admin_notes' ] );
		}

		static::$is_initialized = true;
	}

	/**
	 * Initialize the REST API controller.
	 *
	 * @return void
	 */
	public function init_rest_api() {
		$api_controller = new RestController( \WC_Payments::create_api_client() );
		$api_controller->register_routes();
	}

	/**
	 * Initialize the legacy widgets.
	 *
	 * @return void
	 */
	public function init_widgets() {
		// Register the legacy widget.
		$this->currency_switcher_widget = new CurrencySwitcherWidget( $this, $this->compatibility );
		register_widget( $this->currency_switcher_widget );
	}

	/**
	 * Initialize the Settings Pages.
	 *
	 * @param array $settings_pages The settings pages.
	 *
	 * @return array The new settings pages.
	 */
	public function init_settings_pages( $settings_pages ): array {
		// We don't need to check if Stripe is connected for the
		// Settings page generation on the incoming CLI and async job calls.
		if ( ( defined( 'WP_CLI' ) && WP_CLI ) || ( defined( 'WPCOM_JOBS' ) && WPCOM_JOBS ) ) {
			return $settings_pages;
		}

		if ( $this->payments_account->is_stripe_connected() ) {
			$settings_pages[] = new Settings( $this );
		} else {
			$settings_pages[] = new SettingsOnboardCta( $this );
		}

		return $settings_pages;
	}

	/**
	 * Load the admin assets.
	 *
	 * @return void
	 */
	public function enqueue_admin_scripts() {
		global $current_tab;

		// Output the settings JS and CSS only on the settings page.
		if ( 'wcpay_multi_currency' === $current_tab ) {
			$this->register_admin_scripts();
			wp_enqueue_script( 'WCPAY_MULTI_CURRENCY_SETTINGS' );
			wp_enqueue_style( 'WCPAY_MULTI_CURRENCY_SETTINGS' );
		}
	}

	/**
	 * Wipes the cached currency data option, forcing to re-fetch the data from WPCOM.
	 *
	 * @return void
	 */
	public function clear_cache() {
		Logger::debug( 'Clearing the cache to force new rates to be fetched from the server.' );
		delete_option( self::CURRENCY_CACHE_OPTION );
	}

	/**
	 * Gets and caches the data for the currency rates from the server.
	 * Will be returned as an array with three keys, 'currencies' (the currencies), 'expires' (the expiry time)
	 * and 'updated' (when this data was fetched from the API).
	 *
	 * @return ?array
	 */
	public function get_cached_currencies() {

		$error_expires = get_option( self::CURRENCY_RETRIEVAL_ERROR_OPTION, 0 );
		$cache_data    = $this->read_currencies_from_cache();

		// If the error has not expired, return cached data or null and do not attempt another API call.
		if ( $error_expires > time() ) {
			return $cache_data ?? null;
		}

		// If an array of currencies was returned from the cache and has not expired, return it here.
		if ( null !== $cache_data && $cache_data['expires'] > time() ) {
			return $cache_data;
		}

		// If connection to server cannot be established, or if Stripe is not connected, or if the account is rejected, return expired data or null.
		if ( ! $this->payments_api_client->is_server_connected() || ! $this->payments_account->is_stripe_connected() || $this->payments_account->is_account_rejected() ) {
			return $cache_data ?? null;
		}

		// If the cache was expired or something went wrong, make a call to the server to get the currency data.
		try {
			$currency_data = $this->payments_api_client->get_currency_rates( get_woocommerce_currency() );
		} catch ( API_Exception $e ) {
			// Failed to retrieve currencies from the server. Exception is logged in http client.
			// Rate limit for a short amount of time by persisting the failure.
			update_option( self::CURRENCY_RETRIEVAL_ERROR_OPTION, time() + 1 * MINUTE_IN_SECONDS, 'no' );

			// Return expired data or null to signal currency retrieval error.
			return $cache_data ?? null;
		}

		$updated = time();

		// Cache the currency data so we don't call the server every time.
		$this->cache_currencies( $currency_data, $updated, 6 * HOUR_IN_SECONDS );

		return [
			'currencies' => $currency_data,
			'updated'    => $updated,
		];
	}

	/**
	 * Returns the Compatibility instance.
	 *
	 * @return Compatibility
	 */
	public function get_compatibility() {
		return $this->compatibility;
	}

	/**
	 * Returns the Currency Switcher Widget instance.
	 *
	 * @return CurrencySwitcherWidget|null
	 */
	public function get_currency_switcher_widget() {
		return $this->currency_switcher_widget;
	}

	/**
	 * Returns the FrontendPrices instance.
	 *
	 * @return FrontendPrices
	 */
	public function get_frontend_prices(): FrontendPrices {
		return $this->frontend_prices;
	}

	/**
	 * Returns the FrontendCurrencies instance.
	 *
	 * @return FrontendCurrencies
	 */
	public function get_frontend_currencies(): FrontendCurrencies {
		return $this->frontend_currencies;
	}

	/**
	 * Returns the StorefrontIntegration instance.
	 *
	 * @return StorefrontIntegration|null
	 */
	public function get_storefront_integration() {
		return $this->storefront_integration;
	}

	/**
	 * Generates the switcher widget markup.
	 *
	 * @param array $instance The widget's instance settings.
	 * @param array $args     The widget's arguments.
	 *
	 * @return string The widget markup.
	 */
	public function get_switcher_widget_markup( array $instance = [], array $args = [] ): string {
		/**
		 * The spl_object_hash function is used here due to we register the widget with an instance of the widget and
		 * not the class name of the widget. WordPress core takes the instance and passes it through spl_object_hash
		 * to get a hash and adds that as the widget's name in the $wp_widget_factory->widgets[] array. In order to
		 * call the_widget, you need to have the name of the widget, so we get the instance and hash to use.
		 */
		ob_start();
		the_widget(
			spl_object_hash( $this->get_currency_switcher_widget() ),
			apply_filters( self::FILTER_PREFIX . 'theme_widget_instance', $instance ),
			apply_filters( self::FILTER_PREFIX . 'theme_widget_args', $args )
		);
		return ob_get_clean();
	}

	/**
	 * Sets up the available currencies, which are alphabetical by name.
	 *
	 * @return void
	 */
	private function initialize_available_currencies() {
		// Add default store currency with a rate of 1.0.
		$woocommerce_currency                                = get_woocommerce_currency();
		$this->available_currencies[ $woocommerce_currency ] = new Currency( $woocommerce_currency, 1.0 );

		$available_currencies = [];

		$currencies = $this->get_account_available_currencies();
		$cache_data = $this->get_cached_currencies();

		foreach ( $currencies as $currency_code ) {
			$currency_rate = $cache_data['currencies'][ $currency_code ] ?? 1.0;
			$update_time   = $cache_data['updated'] ?? null;
			$new_currency  = new Currency( $currency_code, $currency_rate, $update_time );

			// Add this to our list of available currencies.
			$available_currencies[ $new_currency->get_name() ] = $new_currency;
		}

		ksort( $available_currencies );

		foreach ( $available_currencies as $currency ) {
			$this->available_currencies[ $currency->get_code() ] = $currency;
		}
	}

	/**
	 * Sets up the enabled currencies.
	 *
	 * @return void
	 */
	private function initialize_enabled_currencies() {
		$available_currencies     = $this->get_available_currencies();
		$enabled_currency_codes   = get_option( $this->id . '_enabled_currencies', [] );
		$enabled_currency_codes   = is_array( $enabled_currency_codes ) ? $enabled_currency_codes : [];
		$default_code             = $this->get_default_currency()->get_code();
		$default                  = [];
		$enabled_currency_codes[] = $default_code;

		// This allows to keep the alphabetical sorting by name.
		$enabled_currencies = array_filter(
			$available_currencies,
			function( $currency ) use ( $enabled_currency_codes ) {
				return in_array( $currency->get_code(), $enabled_currency_codes, true );
			}
		);

		$this->enabled_currencies = [];

		foreach ( $enabled_currencies as $enabled_currency ) {
			// Get the charm and rounding for each enabled currency and add the currencies to the object property.
			$currency = clone $enabled_currency;
			$charm    = get_option( $this->id . '_price_charm_' . $currency->get_id(), 0.00 );
			$rounding = get_option( $this->id . '_price_rounding_' . $currency->get_id(), $currency->get_is_zero_decimal() ? '100' : '1.00' );
			$currency->set_charm( $charm );
			$currency->set_rounding( $rounding );

			// If the currency is set to be manual, set the rate to the stored manual rate.
			$type = get_option( $this->id . '_exchange_rate_' . $currency->get_id(), 'automatic' );
			if ( 'manual' === $type ) {
				$manual_rate = get_option( $this->id . '_manual_rate_' . $currency->get_id(), $currency->get_rate() );
				$currency->set_rate( $manual_rate );
			}

			$this->enabled_currencies[ $currency->get_code() ] = $currency;
		}

		// Set default currency to the top of the list.
		$default[ $default_code ] = $this->enabled_currencies[ $default_code ];
		unset( $this->enabled_currencies[ $default_code ] );
		$this->enabled_currencies = array_merge( $default, $this->enabled_currencies );
	}

	/**
	 * Sets the default currency.
	 *
	 * @return void
	 */
	private function set_default_currency() {
		$available_currencies   = $this->get_available_currencies();
		$this->default_currency = $available_currencies[ get_woocommerce_currency() ] ?? null;
	}

	/**
	 * Gets the currencies available. Initializes it if needed.
	 *
	 * @return Currency[] Array of Currency objects.
	 */
	public function get_available_currencies(): array {
		if ( null === $this->available_currencies ) {
			$this->init();
		}

		return $this->available_currencies ?? [];
	}

	/**
	 * Gets the store base currency. Initializes it if needed.
	 *
	 * @return Currency The store base currency.
	 */
	public function get_default_currency(): Currency {
		if ( null === $this->default_currency ) {
			$this->init();
		}

		return $this->default_currency ?? new Currency( get_woocommerce_currency() );
	}

	/**
	 * Gets the currently enabled currencies. Initializes it if needed.
	 *
	 * @return Currency[] Array of Currency objects.
	 */
	public function get_enabled_currencies(): array {
		if ( null === $this->enabled_currencies ) {
			$this->init();
		}

		return $this->enabled_currencies ?? [];
	}

	/**
	 * Sets the enabled currencies for the store.
	 *
	 * @param string[] $currencies Array of currency codes to be enabled.
	 *
	 * @return void
	 */
	public function set_enabled_currencies( $currencies = [] ) {
		if ( 0 < count( $currencies ) ) {
			// Get the currencies that were removed before they are updated.
			$removed_currencies = array_diff( array_keys( $this->get_enabled_currencies() ), $currencies );

			// Update the enabled currencies and reinitialize.
			update_option( $this->id . '_enabled_currencies', $currencies );
			$this->initialize_enabled_currencies();

			Logger::debug(
				'Enabled currencies updated: '
				. var_export( $currencies, true ) // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_var_export
			);

			// Now remove the removed currencies settings.
			$this->remove_currencies_settings( $removed_currencies );
		}
	}

	/**
	 * Gets the user selected currency, or `$default_currency` if is not set.
	 *
	 * @return Currency
	 */
	public function get_selected_currency(): Currency {
		$multi_currency_code = $this->compatibility->override_selected_currency();
		$currency_code       = $multi_currency_code ? $multi_currency_code : $this->get_stored_currency_code();

		return $this->get_enabled_currencies()[ $currency_code ] ?? $this->get_default_currency();
	}

	/**
	 * Update the selected currency from a currency code.
	 *
	 * @param string $currency_code Three letter currency code.
	 *
	 * @return void
	 */
	public function update_selected_currency( string $currency_code ) {
		$code     = strtoupper( $currency_code );
		$user_id  = get_current_user_id();
		$currency = $this->get_enabled_currencies()[ $code ] ?? null;

		// We discard the cache for the front-end.
		$this->frontend_currencies->selected_currency_changed();

		if ( null === $currency ) {
			return;
		}

		if ( 0 === $user_id && WC()->session ) {
			WC()->session->set( self::CURRENCY_SESSION_KEY, $currency->get_code() );
			// Set the session cookie if is not yet to persist the selected currency.
			if ( ! WC()->session->has_session() && ! headers_sent() ) {
				WC()->session->set_customer_session_cookie( true );
			}
		} elseif ( $user_id ) {
			update_user_meta( $user_id, self::CURRENCY_META_KEY, $currency->get_code() );
		}

		// Recalculate cart when currency changes.
		if ( did_action( 'wp_loaded' ) ) {
			$this->recalculate_cart();
		} else {
			add_action( 'wp_loaded', [ $this, 'recalculate_cart' ] );
		}
	}

	/**
	 * Update the selected currency from url param `currency`.
	 *
	 * @return void
	 */
	public function update_selected_currency_by_url() {
		if ( ! isset( $_GET['currency'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
			return;
		}

		$this->update_selected_currency( sanitize_text_field( wp_unslash( $_GET['currency'] ) ) ); // phpcs:ignore WordPress.Security.NonceVerification
	}

	/**
	 * Update the selected currency from the user's geolocation country.
	 *
	 * @return void
	 */
	public function update_selected_currency_by_geolocation() {
		// We only want to automatically set the currency if this option is enabled.
		if ( ! $this->is_using_auto_currency_switching() ) {
			return;
		}

		// Display notice, prevent duplicates in simulation.
		if ( ! has_action( 'wp_footer', [ $this, 'display_geolocation_currency_update_notice' ] ) ) {
			add_action( 'wp_footer', [ $this, 'display_geolocation_currency_update_notice' ] );
		}

		// Update currency only if it's already not set.
		if ( $this->get_stored_currency_code() ) {
			return;
		}

		$currency = $this->geolocation->get_currency_by_customer_location();

		if ( empty( $this->get_enabled_currencies()[ $currency ] ) ) {
			return;
		}

		$this->update_selected_currency( $currency );
	}

	/**
	 * Gets the configured value for apply charm pricing only to products.
	 *
	 * @return mixed The configured value.
	 */
	public function get_apply_charm_only_to_products() {
		return apply_filters( self::FILTER_PREFIX . 'apply_charm_only_to_products', true );
	}

	/**
	 * Gets the converted price using the current currency with the rounding and charm pricing settings.
	 *
	 * @param mixed  $price The price to be converted.
	 * @param string $type The type of price being converted. One of 'product', 'shipping', 'tax', 'coupon', or 'exchange_rate'.
	 *
	 * @return float The converted price.
	 */
	public function get_price( $price, string $type ): float {
		$supported_types = [ 'product', 'shipping', 'tax', 'coupon', 'exchange_rate' ];
		$currency        = $this->get_selected_currency();

		if ( ! in_array( $type, $supported_types, true ) || $currency->get_is_default() ) {
			return (float) $price;
		}

		$converted_price = ( (float) $price ) * $currency->get_rate();

		if ( 'tax' === $type || 'coupon' === $type || 'exchange_rate' === $type ) {
			return $converted_price;
		}

		$charm_compatible_types = [ 'product', 'shipping' ];
		$apply_charm_pricing    = $this->get_apply_charm_only_to_products()
		? 'product' === $type
		: in_array( $type, $charm_compatible_types, true );

		return $this->get_adjusted_price( $converted_price, $apply_charm_pricing, $currency );
	}

	/**
	 * Recalculates WooCommerce cart totals.
	 *
	 * @return void
	 */
	public function recalculate_cart() {
		WC()->cart->calculate_totals();
	}

	/**
	 * When an order is refunded, a new psuedo order is created to represent the refund.
	 * We want to check if the original order was a multi-currency order, and if so, copy the meta data
	 * to the new order.
	 *
	 * @param int $order_id The order ID.
	 * @param int $refund_id The refund order ID.
	 */
	public function add_order_meta_on_refund( $order_id, $refund_id ) {
		$default_currency = $this->get_default_currency();

		$order  = wc_get_order( $order_id );
		$refund = wc_get_order( $refund_id );

		// Do not add exchange rate if order was made in the store's default currency.
		if ( ! $order || ! $refund || $default_currency->get_code() === $order->get_currency() ) {
			return;
		}

		$order_exchange_rate    = $order->get_meta( '_wcpay_multi_currency_order_exchange_rate', true );
		$stripe_exchange_rate   = $order->get_meta( '_wcpay_multi_currency_stripe_exchange_rate', true );
		$order_default_currency = $order->get_meta( '_wcpay_multi_currency_order_default_currency', true );

		$refund->update_meta_data( '_wcpay_multi_currency_order_exchange_rate', $order_exchange_rate );
		$refund->update_meta_data( '_wcpay_multi_currency_order_default_currency', $order_default_currency );
		if ( $stripe_exchange_rate ) {
			$refund->update_meta_data( '_wcpay_multi_currency_stripe_exchange_rate', $stripe_exchange_rate );
		}

		$refund->save_meta_data();
	}

	/**
	 * Displays a notice on the frontend informing the customer of the
	 * automatic currency switch.
	 */
	public function display_geolocation_currency_update_notice() {
		$current_currency    = $this->get_selected_currency();
		$store_currency      = get_option( 'woocommerce_currency' );
		$country             = $this->geolocation->get_country_by_customer_location();
		$geolocated_currency = $this->geolocation->get_currency_by_customer_location();
		$currencies          = get_woocommerce_currencies();

		// Don't run next checks if simulation is enabled.
		if ( ! $this->is_simulation_enabled() ) {
			// Do not display notice if using the store's default currency.
			if ( $store_currency === $current_currency->get_code() ) {
				return;
			}

			// Do not display notice for other currencies than geolocated.
			if ( $current_currency->get_code() !== $geolocated_currency ) {
				return;
			}
		}

		$message = sprintf(
		/* translators: %1 User's country, %2 Selected currency name, %3 Default store currency name */
			__( 'We noticed you\'re visiting from %1$s. We\'ve updated our prices to %2$s for your shopping convenience. <a>Use %3$s instead.</a>', 'woocommerce-payments' ),
			apply_filters( self::FILTER_PREFIX . 'override_notice_country', WC()->countries->countries[ $country ] ),
			apply_filters( self::FILTER_PREFIX . 'override_notice_currency_name', $current_currency->get_name() ),
			$currencies[ $store_currency ]
		);

		$notice_id = md5( $message );

		echo '<p class="woocommerce-store-notice demo_store" data-notice-id="' . esc_attr( $notice_id . 2 ) . '" style="display:none;">';
		echo \WC_Payments_Utils::esc_interpolated_html(
			$message,
			[
				'a' => '<a href="?currency=' . $store_currency . '">',
			]
		);
		echo ' <a href="#" class="woocommerce-store-notice__dismiss-link">' . esc_html__( 'Dismiss', 'woocommerce-payments' ) . '</a></p>';
	}

	/**
	 * Sets a new customer's currency meta to what's in their session.
	 * This is needed for when a new user/customer is created during the checkout process.
	 *
	 * @param int $customer_id The user/customer id.
	 *
	 * @return void
	 */
	public function set_new_customer_currency_meta( $customer_id ) {
		$code = 0 !== $customer_id && WC()->session ? WC()->session->get( self::CURRENCY_SESSION_KEY ) : false;
		if ( $code ) {
			update_user_meta( $customer_id, self::CURRENCY_META_KEY, $code );
		}
	}

	/**
	 * Adds Multi-Currency notes to the WC-Admin inbox.
	 *
	 * @return void
	 */
	public static function add_woo_admin_notes() {
		// Do not try to add notes on ajax requests to improve their performance.
		if ( wp_doing_ajax() ) {
			return;
		}

		if ( defined( 'WC_VERSION' ) && version_compare( WC_VERSION, '4.4.0', '>=' ) ) {
			NoteMultiCurrencyAvailable::possibly_add_note();
		}
	}

	/**
	 * Removes Multi-Currency notes from the WC-Admin inbox.
	 *
	 * @return void
	 */
	public static function remove_woo_admin_notes() {
		if ( defined( 'WC_VERSION' ) && version_compare( WC_VERSION, '4.4.0', '>=' ) ) {
			NoteMultiCurrencyAvailable::possibly_delete_note();
		}
	}

	/**
	 * Gets the price after adjusting it with the rounding and charm settings.
	 *
	 * @param float    $price               The price to be adjusted.
	 * @param bool     $apply_charm_pricing Whether charm pricing should be applied.
	 * @param Currency $currency The currency to be used when adjusting.
	 *
	 * @return float The adjusted price.
	 */
	protected function get_adjusted_price( $price, $apply_charm_pricing, $currency ): float {
		$price = $this->ceil_price( $price, (float) $currency->get_rounding() );

		if ( $apply_charm_pricing ) {
			$price += (float) $currency->get_charm();
		}

		// Do not return negative prices (possible because of $currency->get_charm()).
		return max( 0, $price );
	}

	/**
	 * Ceils the price to the next number based on the rounding value.
	 *
	 * @param float $price    The price to be ceiled.
	 * @param float $rounding The rounding option.
	 *
	 * @return float The ceiled price.
	 */
	protected function ceil_price( float $price, float $rounding ): float {
		if ( 0.00 === $rounding ) {
			return $price;
		}
		return ceil( $price / $rounding ) * $rounding;
	}

	/**
	 * Returns the currency code stored for the user or in the session.
	 *
	 * @return string|null Currency code.
	 */
	private function get_stored_currency_code() {
		$user_id = get_current_user_id();

		if ( $user_id ) {
			return get_user_meta( $user_id, self::CURRENCY_META_KEY, true );
		}

		WC()->initialize_session();
		$currency_code = WC()->session->get( self::CURRENCY_SESSION_KEY );

		return is_string( $currency_code ) ? $currency_code : null;
	}

	/**
	 * Caches currency data for a period of time.
	 *
	 * @param array    $currencies - Currency data to cache.
	 * @param int|null $updated    - The time the data was fetched from the server.
	 * @param int|null $expiration - The length of time to cache the currency data, in seconds.
	 *
	 * @return bool
	 */
	private function cache_currencies( $currencies, int $updated = null, int $expiration = null ): bool {
		// Default $expiration to 6 hours if not set.
		if ( null === $expiration ) {
			$expiration = 6 * HOUR_IN_SECONDS;
		}

		// Default $updated to the current time.
		if ( null === $updated ) {
			$updated = time();
		}

		// Add the currency data, expiry time, and time updated to the array we're caching.
		$currency_cache = [
			'currencies' => $currencies,
			'expires'    => time() + $expiration,
			'updated'    => $updated,
		];

		Logger::debug( 'Saved currencies fetched from the server to the cache. Expiry: ' . gmdate( 'Y-m-d H:i:s', time() + $expiration ) );
		return update_option( self::CURRENCY_CACHE_OPTION, $currency_cache, 'no' );
	}

	/**
	 * Checks to see if the store currency has changed. If it has, this will
	 * also update the option containing the store currency.
	 *
	 * @return bool
	 */
	private function check_store_currency_for_change(): bool {
		$last_known_currency  = get_option( $this->id . '_store_currency', false );
		$woocommerce_currency = get_woocommerce_currency();

		// If the last known currency was not set, update the option to set it and return false.
		if ( ! $last_known_currency ) {
			update_option( $this->id . '_store_currency', $woocommerce_currency );
			return false;
		}

		if ( $last_known_currency !== $woocommerce_currency ) {
			update_option( $this->id . '_store_currency', $woocommerce_currency );
			return true;
		}

		return false;
	}

	/**
	 * Called when the store currency has changed. Puts any manual rate currencies into an option for a notice to display.
	 *
	 * @return void
	 */
	private function update_manual_rate_currencies_notice_option() {
		$enabled_currencies = $this->get_enabled_currencies();
		$manual_currencies  = [];

		// Check enabled currencies for manual rates.
		foreach ( $enabled_currencies as $currency ) {
			$rate_type = get_option( $this->id . '_exchange_rate_' . $currency->get_id(), false );
			if ( 'manual' === $rate_type ) {
				$manual_currencies[] = $currency->get_name();
			}
		}

		if ( 0 < count( $manual_currencies ) ) {
			update_option( $this->id . '_show_store_currency_changed_notice', $manual_currencies );
		}
	}

	/**
	 * Read the currency data from the WP option we cache it in.
	 *
	 * @return ?array
	 */
	private function read_currencies_from_cache() {
		$currency_cache = get_option( self::CURRENCY_CACHE_OPTION, [] );

		if ( ! is_array( $currency_cache )
			|| ! isset( $currency_cache['currencies'] )
			|| ! isset( $currency_cache['expires'] )
			|| ! isset( $currency_cache['updated'] ) ) {
			// No option found or the data isn't in the format we expect.
			return null;
		}

		// We have fresh currency data in the cache, so return it.
		return $currency_cache;
	}

	/**
	 * Accepts an array of currencies that should have their settings removed.
	 *
	 * @param array $currencies Array of Currency objects or 3 letter currency codes.
	 *
	 * @return void
	 */
	private function remove_currencies_settings( array $currencies ) {

		foreach ( $currencies as $currency ) {
			$this->remove_currency_settings( $currency );
		}
	}

	/**
	 * Will remove a currency's settings if it is not enabled.
	 *
	 * @param mixed $currency Currency object or 3 letter currency code.
	 *
	 * @return void
	 */
	private function remove_currency_settings( $currency ) {
		$code = is_a( $currency, Currency::class ) ? $currency->get_code() : strtoupper( $currency );

		// Bail if the currency code passed is not 3 characters, or if the currency is presently enabled.
		if ( 3 !== strlen( $code ) || isset( $this->get_enabled_currencies()[ $code ] ) ) {
			return;
		}

		$settings = [
			'price_charm',
			'price_rounding',
			'manual_rate',
			'exchange_rate',
		];

		// Go through each setting and remove them.
		foreach ( $settings as $setting ) {
			delete_option( $this->id . '_' . $setting . '_' . strtolower( $code ) );
		}
	}

	/**
	 * Returns the currencies enabled for the Stripe account that are
	 * also available in WC.
	 *
	 * Can be filtered with the 'wcpay_multi_currency_available_currencies' hook.
	 *
	 * @return array Array with the available currencies' codes.
	 */
	private function get_account_available_currencies(): array {
		// If Stripe is not connected, return an empty array. This prevents using MC without being connected to Stripe.
		if ( ! $this->payments_account->is_stripe_connected() ) {
			return [];
		}

		$wc_currencies      = array_keys( get_woocommerce_currencies() );
		$account_currencies = $wc_currencies;

		$account              = $this->payments_account->get_cached_account_data();
		$supported_currencies = $this->payments_account->get_account_customer_supported_currencies();
		if ( $account && ! empty( $supported_currencies ) ) {
			$account_currencies = array_map( 'strtoupper', $supported_currencies );
		}

		/**
		 * Filter the available currencies for WooCommerce Multi-Currency.
		 *
		 * This filter can be used to modify the currencies available for WC Pay
		 * Multi-Currency. Currencies have to be added in uppercase and should
		 * also be available in `get_woocommerce_currencies` for them to work.
		 *
		 * @since 2.8.0
		 *
		 * @param array $available_currencies Current available currencies. Calculated based on
		 *                                    WC Pay's account currencies and WC currencies.
		 */
		return apply_filters( self::FILTER_PREFIX . 'available_currencies', array_intersect( $account_currencies, $wc_currencies ) );
	}

	/**
	 * Checks if the merchant has enabled automatic currency switching and geolocation.
	 *
	 * @return bool
	 */
	public function is_using_auto_currency_switching(): bool {
		return 'yes' === get_option( $this->id . '_enable_auto_currency', 'no' );
	}

	/**
	 * Checks if the merchant has enabled the currency switcher widget.
	 *
	 * @return  bool
	 */
	public function is_using_storefront_switcher(): bool {
		return 'yes' === get_option( $this->id . '_enable_storefront_switcher', 'no' );
	}

	/**
	 * Gets the store settings.
	 *
	 * @return  array  The store settings.
	 */
	public function get_settings() {
		return [
			$this->id . '_enable_auto_currency'       => $this->is_using_auto_currency_switching(),
			$this->id . '_enable_storefront_switcher' => $this->is_using_storefront_switcher(),
			'site_theme'                              => wp_get_theme()->get( 'Name' ),
			'date_format'                             => esc_attr( get_option( 'date_format', 'F j, Y' ) ),
			'time_format'                             => esc_attr( get_option( 'time_format', 'g:i a' ) ),
			'store_url'                               => esc_attr( get_page_uri( wc_get_page_id( 'shop' ) ) ),
		];
	}

	/**
	 * Updates the store settings
	 *
	 * @param   array $params  Update requested values.
	 *
	 * @return  void
	 */
	public function update_settings( $params ) {
		$updateable_options = [
			'wcpay_multi_currency_enable_auto_currency',
			'wcpay_multi_currency_enable_storefront_switcher',
		];

		foreach ( $updateable_options as $key ) {
			if ( isset( $params[ $key ] ) ) {
				update_option( $key, sanitize_text_field( $params[ $key ] ) );
			}
		}
	}

	/**
	 * Apply client order currency format and reduces the rounding precision to 2.
	 *
	 * @return  void
	 */
	public function set_client_format_and_rounding_precision() {
		$screen = get_current_screen();
		if ( 'post' === $screen->base && 'shop_order' === $screen->post_type ) :
			global $post;
			$currency                     = wc_get_order( $post->ID )->get_currency();
			$currency_format_num_decimals = $this->backend_currencies->get_price_decimals( $currency );
			$currency_format_decimal_sep  = $this->backend_currencies->get_price_decimal_separator( $currency );
			$currency_format_thousand_sep = $this->backend_currencies->get_price_thousand_separator( $currency );
			$currency_format              = str_replace( [ '%1$s', '%2$s', '&nbsp;' ], [ '%s', '%v', ' ' ], $this->backend_currencies->get_woocommerce_price_format( $currency ) );

			$rounding_precision = wc_get_price_decimals() ?? wc_get_rounding_precision();
			?>
		<script>
		woocommerce_admin_meta_boxes.currency_format_num_decimals = <?php echo (int) $currency_format_num_decimals; ?>;
		woocommerce_admin_meta_boxes.currency_format_decimal_sep = '<?php echo esc_attr( $currency_format_decimal_sep ); ?>';
		woocommerce_admin_meta_boxes.currency_format_thousand_sep = '<?php echo esc_attr( $currency_format_thousand_sep ); ?>';
		woocommerce_admin_meta_boxes.currency_format = '<?php echo esc_attr( $currency_format ); ?>';
		woocommerce_admin_meta_boxes.rounding_precision = <?php echo (int) $rounding_precision; ?>;
		</script>
			<?php
		endif;
	}

	/**
	 * Register the CSS and JS admin scripts.
	 *
	 * @return void
	 */
	private function register_admin_scripts() {
		$script_src_url    = plugins_url( 'dist/multi-currency.js', WCPAY_PLUGIN_FILE );
		$script_asset_path = WCPAY_ABSPATH . 'dist/multi-currency.asset.php';
		$script_asset      = file_exists( $script_asset_path ) ? require_once $script_asset_path : [ 'dependencies' => [] ];
		wp_register_script(
			'WCPAY_MULTI_CURRENCY_SETTINGS',
			$script_src_url,
			$script_asset['dependencies'],
			\WC_Payments::get_file_version( 'dist/multi-currency.js' ),
			true
		);

		wp_register_style(
			'WCPAY_MULTI_CURRENCY_SETTINGS',
			plugins_url( 'dist/multi-currency.css', WCPAY_PLUGIN_FILE ),
			[ 'wc-components' ],
			\WC_Payments::get_file_version( 'dist/multi-currency.css' )
		);
	}

	/**
	 * Validates the given currency code.
	 *
	 * @param   string $currency_code  The currency code to check validity.
	 *
	 * @return  string|false  Returns back the currency code in uppercase letters if it's valid, or `false` if not.
	 */
	public function validate_currency_code( $currency_code ) {
		return array_key_exists( strtoupper( $currency_code ), $this->available_currencies )
		? strtoupper( $currency_code )
		: false;
	}

	/**
	 * Get simulation params from querystring and activate when needed
	 *
	 * @return  void
	 */
	public function possible_simulation_activation() {
		// This is required in the MC onboarding simulation iframe.
		$this->simulation_params = $this->get_multi_currency_onboarding_simulation_variables();
		if ( ! $this->is_simulation_enabled() ) {
			return;
		}
		// Modify the page links to deliver required params in the simulation.
		$this->add_simulation_params_to_preview_urls();
		$this->simulate_client_currency();
	}

	/**
	 * Enables simulation of client browser currency.
	 *
	 * @return  void
	 */
	private function simulate_client_currency() {
		if ( ! $this->simulation_params['enable_auto_currency'] ) {
			return;
		}

		$countries = WC_Payments_Utils::supported_countries();

		$predefined_simulation_currencies = [
			'USD' => $countries['US'],
			'GBP' => $countries['GB'],
		];

		$simulation_currency      = 'USD' === get_option( 'woocommerce_currency', 'USD' ) ? 'GBP' : 'USD';
		$simulation_currency_name = $this->available_currencies[ $simulation_currency ]->get_name();
		$simulation_country       = $predefined_simulation_currencies[ $simulation_currency ];

		// Simulate client currency from geolocation.
		add_filter(
			'wcpay_multi_currency_override_notice_currency_name',
			function( $selected_currency_name ) use ( $simulation_currency_name ) {
				return $simulation_currency_name;
			}
		);

		// Simulate client country from geolocation.
		add_filter(
			'wcpay_multi_currency_override_notice_country',
			function( $selected_country ) use ( $simulation_country ) {
				return $simulation_country;
			}
		);

		// Always display the notice on simulation screen, prevent duplicate hooks.
		if ( ! has_action( 'wp_footer', [ $this, 'display_geolocation_currency_update_notice' ] ) ) {
			add_action( 'wp_footer', [ $this, 'display_geolocation_currency_update_notice' ] );
		}

		// Skip recalculating the cart to prevent infinite loop in simulation.
		remove_action( 'wp_loaded', [ $this, 'recalculate_cart' ] );

	}

	/**
	 * Returns whether the simulation querystring param is set and active
	 *
	 * @return  bool  Whether the simulation is enabled or not
	 */
	public function is_simulation_enabled() {
		return 0 < count( $this->simulation_params );
	}

	/**
	 * Gets the Multi-Currency onboarding preview overrides from the querystring.
	 *
	 * @return  array  Override variables
	 */
	public function get_multi_currency_onboarding_simulation_variables() {

		$parameters = $_GET; // phpcs:ignore WordPress.Security.NonceVerification
		// Check if we are in a preview session, don't interfere with the main session.
		if ( ! isset( $parameters['is_mc_onboarding_simulation'] ) || ! (bool) $parameters['is_mc_onboarding_simulation'] ) {
			// Check if the page referer has the variables.
			$server = $_SERVER; // phpcs:ignore WordPress.Security.NonceVerification
			// Check if we are coming from a simulation session (if we don't have the necessary query strings).
			if ( isset( $server['HTTP_REFERER'] ) && 0 < strpos( $server['HTTP_REFERER'], 'is_mc_onboarding_simulation' ) ) {
				wp_parse_str( wp_parse_url( $server['HTTP_REFERER'], PHP_URL_QUERY ), $parameters );
				if ( ! isset( $parameters['is_mc_onboarding_simulation'] ) || ! (bool) $parameters['is_mc_onboarding_simulation'] ) {
					return [];
				}
			} else {
				return [];
			}
		}

		// Define variables which can be overridden inside the preview session, with their sanitization methods.
		$possible_variables = [
			'enable_storefront_switcher' => 'wp_validate_boolean',
			'enable_auto_currency'       => 'wp_validate_boolean',
		];

		// Define the defaults if the parameter is missing in the request.
		$defaults = [
			'enable_storefront_switcher' => false,
			'enable_auto_currency'       => false,
		];

		// Prepare the params array.
		$values = [];

		// Walk through the querystring parameter possibilities, and prepare the params.
		foreach ( $possible_variables as $possible_variable => $sanitization_callback ) {
			// phpcs:disable WordPress.Security.NonceVerification
			if ( isset( $parameters[ $possible_variable ] ) ) {
				$values[ $possible_variable ] = $sanitization_callback( $parameters[ $possible_variable ] );
			} else {
				// Append the default, the param is missing in the querystring.
				$values [ $possible_variable ] = $defaults[ $possible_variable ];
			}
		}

		return $values;
	}

	/**
	 * Adds the required querystring parameters to all urls in preview pages.
	 *
	 * @return  void
	 */
	private function add_simulation_params_to_preview_urls() {
		$params = $this->simulation_params;
		add_filter(
			'wp_footer',
			function() use ( $params ) {
				?>
			<script type="text/javascript" id="wcpay_multi_currency-simulation-script">
				// Add simulation overrides to all links.
				document.querySelectorAll('a').forEach((link) => {
					const parsedURL = new URL(link.href);
					if (
						false === parsedURL.searchParams.has( 'is_mc_onboarding_simulation' )
					) {
						parsedURL.searchParams.set('is_mc_onboarding_simulation', true);
						parsedURL.searchParams.set('enable_auto_currency', <?php echo esc_attr( $params['enable_auto_currency'] ? 'true' : 'false' ); ?>);
						parsedURL.searchParams.set('enable_storefront_switcher', <?php echo esc_attr( $params['enable_storefront_switcher'] ? 'true' : 'false' ); ?>);
						link.href = parsedURL.toString();
					}
				});

				// Unhide the store notice in simulation mode.
				document.addEventListener('DOMContentLoaded', () => {
					const noticeElement = document.querySelector('.woocommerce-store-notice.demo_store')
					if( noticeElement ) {
						const noticeId = noticeElement.getAttribute('data-notice-id');
						cookieStore.delete( 'store_notice' + noticeId );
					}
				});
			</script>
				<?php
			}
		);
	}

	/**
	 * Checks if the currently displayed page is the WooCommerce Payments
	 * settings page for the Multi-Currency settings.
	 *
	 * @return bool
	 */
	public function is_multi_currency_settings_page(): bool {
		global $current_screen, $current_tab;
		return (
			is_admin()
			&& $current_tab && $current_screen
			&& 'wcpay_multi_currency' === $current_tab
			&& 'woocommerce_page_wc-settings' === $current_screen->base
		);
	}

	/**
	 * Returns if the currency initialization are completed
	 *
	 * @return  bool    If the initializations have been completedÎ
	 */
	public static function is_initialized() : bool {
		return static::$is_initialized;
	}
}
