<?php
/**
 * Storzen Module — Currency Switcher (Pro)
 *
 * Display-only multi-currency support: visitor picks a currency from a
 * widget, all WC prices on the storefront re-render in the chosen currency.
 *
 * IMPORTANT — display-only by design.
 *   Orders are still placed in the store's BASE currency. The switcher
 *   shows converted prices for browsing/comparison; checkout reverts to
 *   base currency. This matches the model used by Aelia Free, Currency
 *   Switcher for WooCommerce, and similar plugins. Full transaction-
 *   currency support would require deep payment-gateway integration
 *   which is out of scope.
 *
 * How rates work:
 *   • `rate_source = auto` → free public API (open.er-api.com), cached in a
 *     transient for `refresh_hours`.
 *   • `rate_source = manual` → admin-supplied rates (set as option
 *     `storzen_currency_manual_rates`, expandable in a future round).
 *
 * @package storzen
 * @since   1.5.3
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

if ( ! class_exists( 'Storzen_Module_Currency_Switcher' ) ) {

	class Storzen_Module_Currency_Switcher {

		const COOKIE_KEY        = 'storzen_currency';
		const TRANSIENT_RATES   = 'storzen_currency_rates';
		const OPTION_MANUAL     = 'storzen_currency_manual_rates';
		const SHORTCODE         = 'storzen_currency_switcher';
		const AJAX_SWITCH       = 'storzen_currency_switch';
		const NONCE             = 'storzen_currency_nonce';

		/** @var array|null Per-request settings */
		private $settings = null;

		/** @var string|null Resolved active currency code */
		private $active = null;

		/** @var array|null Cached rate map */
		private $rates = null;

		/** @var string */
		private $base_currency = '';

		public function __construct() {
			if ( ! function_exists( 'WC' ) ) {
				return;
			}

			$this->base_currency = function_exists( 'get_woocommerce_currency' ) ? get_woocommerce_currency() : 'USD';

			// This module is instantiated at init priority 15 (load_active_modules).
			// By then, init priority 5 has already fired — so we call
			// detect_active_currency() directly here instead of scheduling it.
			$this->detect_active_currency();

			// Apply currency code/symbol overrides.
			add_filter( 'woocommerce_currency',        array( $this, 'filter_currency_code' ),   20 );
			add_filter( 'woocommerce_currency_symbol', array( $this, 'filter_currency_symbol' ), 20, 2 );

			// Convert prices.
			add_filter( 'woocommerce_product_get_price',                  array( $this, 'filter_price' ), 20, 2 );
			add_filter( 'woocommerce_product_get_regular_price',          array( $this, 'filter_price' ), 20, 2 );
			add_filter( 'woocommerce_product_get_sale_price',             array( $this, 'filter_price' ), 20, 2 );
			add_filter( 'woocommerce_product_variation_get_price',        array( $this, 'filter_price' ), 20, 2 );
			add_filter( 'woocommerce_product_variation_get_regular_price',array( $this, 'filter_price' ), 20, 2 );
			add_filter( 'woocommerce_product_variation_get_sale_price',   array( $this, 'filter_price' ), 20, 2 );

			// Variable-product price range (HTML cache key keyed off currency).
			add_filter( 'woocommerce_variation_prices_price',         array( $this, 'filter_variation_price' ), 20, 3 );
			add_filter( 'woocommerce_variation_prices_regular_price', array( $this, 'filter_variation_price' ), 20, 3 );
			add_filter( 'woocommerce_variation_prices_sale_price',    array( $this, 'filter_variation_price' ), 20, 3 );
			add_filter( 'woocommerce_get_variation_prices_hash',      array( $this, 'add_currency_to_hash' ), 20, 3 );

			// Restore base currency at checkout/cart calculation time so orders
			// store in base. This is intentional — see "display-only" note above.
			add_action( 'woocommerce_checkout_create_order', array( $this, 'note_display_currency_on_order' ), 10, 1 );

			// Frontend: assets + sticky widget.
			add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_assets' ) );
			add_action( 'wp_footer',          array( $this, 'render_sticky_widget' ), 30 );

			// Shortcode for theme menu / header placement.
			add_shortcode( self::SHORTCODE, array( $this, 'shortcode_render_switcher' ) );

			// AJAX: change currency.
			add_action( 'wp_ajax_'        . self::AJAX_SWITCH, array( $this, 'ajax_switch' ) );
			add_action( 'wp_ajax_nopriv_' . self::AJAX_SWITCH, array( $this, 'ajax_switch' ) );

			// Scheduled rate refresh.
			add_action( 'storzen_currency_refresh_rates', array( $this, 'refresh_rates_from_api' ) );
			if ( ! wp_next_scheduled( 'storzen_currency_refresh_rates' ) ) {
				wp_schedule_event( time(), 'twicedaily', 'storzen_currency_refresh_rates' );
			}
		}

		// ─────────────────────────────────────────────────────────────────
		// Settings
		// ─────────────────────────────────────────────────────────────────

		private function get_settings() {
			if ( null !== $this->settings ) {
				return $this->settings;
			}

			$enabled = storzen_module_setting( 'currency_switcher', 'enabled_currencies' );
			if ( ! is_array( $enabled ) || empty( $enabled ) ) {
				$enabled = array( 'USD', 'EUR', 'GBP' );
			}
			$enabled = array_map( 'strtoupper', array_map( 'sanitize_text_field', $enabled ) );

			// Ensure base currency is always in the list.
			if ( ! in_array( $this->base_currency, $enabled, true ) ) {
				array_unshift( $enabled, $this->base_currency );
			}

			$rate_source = (string) storzen_module_setting( 'currency_switcher', 'rate_source' );
			if ( ! in_array( $rate_source, array( 'auto', 'manual' ), true ) ) {
				$rate_source = 'auto';
			}

			$valid_style = array( 'dropdown', 'list', 'buttons', 'flags' );
			$style = (string) storzen_module_setting( 'currency_switcher', 'switcher_style' );
			if ( ! in_array( $style, $valid_style, true ) ) {
				$style = 'dropdown';
			}

			$valid_fmt = array( 'code', 'symbol', 'code_symbol', 'name' );
			$fmt = (string) storzen_module_setting( 'currency_switcher', 'display_format' );
			if ( ! in_array( $fmt, $valid_fmt, true ) ) {
				$fmt = 'code_symbol';
			}

			$this->settings = array(
				'enabled_currencies' => array_values( array_unique( $enabled ) ),
				'rate_source'        => $rate_source,
				'refresh_hours'      => max( 1, (int) storzen_module_setting( 'currency_switcher', 'refresh_hours' ) ?: 24 ),
				'auto_detect'        => (bool) storzen_module_setting( 'currency_switcher', 'auto_detect' ),
				'switcher_style'     => $style,
				'display_format'     => $fmt,
				'show_flags'         => (bool) storzen_module_setting( 'currency_switcher', 'show_flags' ),
				'sticky_widget'      => (bool) storzen_module_setting( 'currency_switcher', 'sticky_widget' ),
			);
			return $this->settings;
		}

		// ─────────────────────────────────────────────────────────────────
		// Active currency resolution
		// ─────────────────────────────────────────────────────────────────

		/**
		 * Decide which currency this request should display in.
		 *
		 * Priority:
		 *   1. URL param ?szcur=USD (single-request override)
		 *   2. Cookie storzen_currency
		 *   3. GeoIP auto-detect (if `auto_detect` setting on)
		 *   4. Store base currency
		 */
		public function detect_active_currency() {
			$s = $this->get_settings();

			// URL param (useful for testing, persistent overrides via cookie).
			// phpcs:disable WordPress.Security.NonceVerification.Recommended -- Read-only display preference (currency); no state change requiring a nonce.
			if ( isset( $_GET['szcur'] ) ) {
				$code = strtoupper( sanitize_text_field( wp_unslash( $_GET['szcur'] ) ) );
				// phpcs:enable WordPress.Security.NonceVerification.Recommended
				if ( $this->is_supported( $code, $s ) ) {
					$this->active = $code;
					$this->set_cookie( $code );
					return;
				}
			}

			// Cookie.
			if ( isset( $_COOKIE[ self::COOKIE_KEY ] ) ) {
				$code = strtoupper( sanitize_text_field( wp_unslash( $_COOKIE[ self::COOKIE_KEY ] ) ) );
				if ( $this->is_supported( $code, $s ) ) {
					$this->active = $code;
					return;
				}
			}

			// GeoIP auto-detect.
			if ( $s['auto_detect'] && class_exists( 'WC_Geolocation' ) ) {
				$loc = WC_Geolocation::geolocate_ip();
				if ( ! empty( $loc['country'] ) ) {
					$mapped = $this->country_to_currency( $loc['country'] );
					if ( $mapped && $this->is_supported( $mapped, $s ) ) {
						$this->active = $mapped;
						$this->set_cookie( $mapped );
						return;
					}
				}
			}

			$this->active = $this->base_currency;
		}

		private function is_supported( $code, $settings ) {
			return in_array( $code, $settings['enabled_currencies'], true );
		}

		private function set_cookie( $code ) {
			if ( function_exists( 'wc_setcookie' ) ) {
				wc_setcookie( self::COOKIE_KEY, $code, time() + ( 30 * DAY_IN_SECONDS ) );
			} else {
				setcookie( self::COOKIE_KEY, $code, time() + ( 30 * DAY_IN_SECONDS ), defined( 'COOKIEPATH' ) ? COOKIEPATH : '/' );
			}
			$_COOKIE[ self::COOKIE_KEY ] = $code;
		}

		/**
		 * Very small ISO country → currency map. Covers the ~30 most common
		 * cases; falls back to null otherwise (which means use base currency).
		 *
		 * @param string $country  ISO 3166-1 alpha-2 country code
		 * @return string|null     ISO 4217 currency code
		 */
		private function country_to_currency( $country ) {
			$map = array(
				'US' => 'USD', 'CA' => 'CAD', 'MX' => 'MXN', 'BR' => 'BRL',
				'GB' => 'GBP', 'IE' => 'EUR',
				'DE' => 'EUR', 'FR' => 'EUR', 'IT' => 'EUR', 'ES' => 'EUR',
				'NL' => 'EUR', 'BE' => 'EUR', 'AT' => 'EUR', 'FI' => 'EUR',
				'GR' => 'EUR', 'PT' => 'EUR', 'CY' => 'EUR', 'EE' => 'EUR',
				'LV' => 'EUR', 'LT' => 'EUR', 'LU' => 'EUR', 'MT' => 'EUR',
				'SK' => 'EUR', 'SI' => 'EUR',
				'AU' => 'AUD', 'NZ' => 'AUD',
				'JP' => 'JPY', 'CN' => 'CNY', 'IN' => 'INR',
				'SG' => 'SGD', 'AE' => 'AED', 'BD' => 'BDT',
			);
			return isset( $map[ $country ] ) ? $map[ $country ] : null;
		}

		// ─────────────────────────────────────────────────────────────────
		// Exchange rates
		// ─────────────────────────────────────────────────────────────────

		/**
		 * Get rate map: { currency_code => rate_relative_to_base, ... }
		 * Returns empty array if rates unavailable.
		 *
		 * @return array<string, float>
		 */
		private function get_rates() {
			if ( null !== $this->rates ) {
				return $this->rates;
			}
			$s = $this->get_settings();

			if ( 'manual' === $s['rate_source'] ) {
				$manual = get_option( self::OPTION_MANUAL, array() );
				$this->rates = is_array( $manual ) ? array_map( 'floatval', $manual ) : array();
			} else {
				$cached = get_transient( self::TRANSIENT_RATES );
				if ( is_array( $cached ) && ! empty( $cached ) ) {
					$this->rates = array_map( 'floatval', $cached );
				} else {
					$this->rates = $this->refresh_rates_from_api();
				}
			}

			// Ensure base is 1.0 (sanity).
			$this->rates[ $this->base_currency ] = 1.0;
			return $this->rates;
		}

		/**
		 * Fetch fresh rates from a free public API. Caches in transient.
		 *
		 * Endpoint: https://open.er-api.com/v6/latest/{base}
		 * Format:   { result: "success", rates: { USD: 1.0, EUR: 0.92, ... } }
		 *
		 * @return array<string, float>
		 */
		public function refresh_rates_from_api() {
			$s    = $this->get_settings();
			$url  = 'https://open.er-api.com/v6/latest/' . urlencode( $this->base_currency );
			$resp = wp_remote_get( $url, array( 'timeout' => 8 ) );

			if ( is_wp_error( $resp ) ) {
				return array();
			}
			$code = wp_remote_retrieve_response_code( $resp );
			if ( 200 !== (int) $code ) {
				return array();
			}
			$body = json_decode( wp_remote_retrieve_body( $resp ), true );
			if ( ! is_array( $body ) || empty( $body['rates'] ) || 'success' !== ( $body['result'] ?? '' ) ) {
				return array();
			}

			$rates = array_map( 'floatval', $body['rates'] );
			$rates[ $this->base_currency ] = 1.0;

			// Cache for `refresh_hours`.
			set_transient( self::TRANSIENT_RATES, $rates, $s['refresh_hours'] * HOUR_IN_SECONDS );

			$this->rates = $rates;
			return $rates;
		}

		/**
		 * Convert a base-currency price to the active currency.
		 *
		 * @param float $base_price
		 * @return float
		 */
		private function convert( $base_price ) {
			if ( $this->active === $this->base_currency || empty( $base_price ) ) {
				return (float) $base_price;
			}
			$rates = $this->get_rates();
			if ( empty( $rates[ $this->active ] ) ) {
				return (float) $base_price;
			}
			return round( (float) $base_price * (float) $rates[ $this->active ], 2 );
		}

		// ─────────────────────────────────────────────────────────────────
		// WC filters
		// ─────────────────────────────────────────────────────────────────

		public function filter_currency_code( $code ) {
			return $this->active ?: $code;
		}

		public function filter_currency_symbol( $symbol, $currency ) {
			// WC's get_woocommerce_currency_symbol() passes the active currency.
			// Provide symbols for currencies the WC core may not know.
			$extra = array(
				'BDT' => '৳',
				'AED' => 'د.إ',
			);
			if ( isset( $extra[ $currency ] ) ) {
				return $extra[ $currency ];
			}
			return $symbol;
		}

		/**
		 * Price filter for simple products + variations (non-cached path).
		 *
		 * @param string|float $price
		 * @param WC_Product   $product
		 * @return string|float
		 */
		public function filter_price( $price, $product ) {
			if ( '' === $price || null === $price ) {
				return $price;
			}
			if ( $this->active === $this->base_currency ) {
				return $price;
			}
			// During checkout/order create, WC needs base currency for storage.
			// We detect that by checking if we're inside the order-create path.
			if ( $this->is_in_order_context() ) {
				return $price;
			}
			return (string) $this->convert( $price );
		}

		/**
		 * Variation prices filter — used by WC for the min/max range on
		 * variable products. Same convert logic.
		 *
		 * @param string|float       $price
		 * @param WC_Product_Variation $variation
		 * @param WC_Product_Variable $product
		 * @return string|float
		 */
		public function filter_variation_price( $price, $variation, $product ) {
			if ( '' === $price || null === $price ) {
				return $price;
			}
			if ( $this->active === $this->base_currency ) {
				return $price;
			}
			if ( $this->is_in_order_context() ) {
				return $price;
			}
			return (string) $this->convert( $price );
		}

		/**
		 * Variable-product price hash must include currency so WC re-caches
		 * the min/max range per currency.
		 */
		public function add_currency_to_hash( $hash, $product, $for_display ) {
			$hash['storzen_currency'] = $this->active;
			return $hash;
		}

		/**
		 * Heuristic: are we inside a cart/checkout/order creation context where
		 * WC needs the BASE currency for accurate storage? We avoid converting
		 * in those contexts.
		 *
		 * @return bool
		 */
		private function is_in_order_context() {
			// During AJAX add-to-cart and cart fragments refresh, WC computes
			// totals — we DO want display conversion there.
			// But during actual order creation (checkout submission), it needs base.
			if ( did_action( 'woocommerce_checkout_create_order' ) ) {
				return true;
			}
			if ( did_action( 'woocommerce_new_order' ) ) {
				return true;
			}
			return false;
		}

		/**
		 * Note the customer's displayed currency on the order — even though
		 * the actual order is in base currency, this gives merchants insight.
		 */
		public function note_display_currency_on_order( $order ) {
			if ( $this->active && $this->active !== $this->base_currency ) {
				$order->update_meta_data( '_storzen_display_currency', $this->active );
			}
		}

		// ─────────────────────────────────────────────────────────────────
		// Switcher UI
		// ─────────────────────────────────────────────────────────────────

		public function enqueue_assets() {
			if ( is_admin() ) {
				return;
			}
			wp_enqueue_style(
				'storzen-currency',
				STORZEN_ROOT_URL . 'assets/css/modules/currency-switcher.css',
				array(),
				STORZEN_VERSION
			);
			wp_enqueue_script(
				'storzen-currency',
				STORZEN_ROOT_URL . 'assets/js/modules/currency-switcher.js',
				array( 'jquery' ),
				STORZEN_VERSION,
				true
			);
			wp_localize_script(
				'storzen-currency',
				'szCurrency',
				array(
					'ajaxUrl' => admin_url( 'admin-ajax.php' ),
					'nonce'   => wp_create_nonce( self::NONCE ),
					'active'  => $this->active,
				)
			);
		}

		public function render_sticky_widget() {
			$s = $this->get_settings();
			if ( ! $s['sticky_widget'] || is_admin() ) {
				return;
			}
			echo '<div class="sz-cur-sticky">';
			echo $this->shortcode_render_switcher(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- sub-method escapes
			echo '</div>';
		}

		public function shortcode_render_switcher() {
			$s        = $this->get_settings();
			$style    = $s['switcher_style'];
			$active   = $this->active ?: $this->base_currency;

			ob_start();
			?>
			<div class="sz-cur-switcher sz-cur-switcher--<?php echo esc_attr( $style ); ?>" data-sz-currency-switcher>
				<?php if ( 'dropdown' === $style ) : ?>
					<select class="sz-cur-select" data-sz-currency-select aria-label="<?php esc_attr_e( 'Choose currency', 'storzen' ); ?>">
						<?php foreach ( $s['enabled_currencies'] as $code ) : ?>
							<option value="<?php echo esc_attr( $code ); ?>" <?php selected( $active, $code ); ?>>
								<?php echo esc_html( $this->format_currency_label( $code, $s ) ); ?>
							</option>
						<?php endforeach; ?>
					</select>
				<?php else : ?>
					<ul class="sz-cur-list">
						<?php foreach ( $s['enabled_currencies'] as $code ) :
							$is_active = ( $code === $active );
							?>
							<li>
								<button
									type="button"
									class="sz-cur-item <?php echo $is_active ? 'is-active' : ''; ?>"
									data-sz-currency="<?php echo esc_attr( $code ); ?>"
									aria-pressed="<?php echo $is_active ? 'true' : 'false'; ?>"
								>
									<?php if ( $s['show_flags'] ) : ?>
										<span class="sz-cur-flag"><?php echo esc_html( $this->flag_emoji( $code ) ); ?></span>
									<?php endif; ?>
									<span class="sz-cur-label"><?php echo esc_html( $this->format_currency_label( $code, $s ) ); ?></span>
								</button>
							</li>
						<?php endforeach; ?>
					</ul>
				<?php endif; ?>
			</div>
			<?php
			return ob_get_clean();
		}

		/**
		 * Format the visible label for a currency, per display_format setting.
		 */
		private function format_currency_label( $code, $settings ) {
			$symbol = function_exists( 'get_woocommerce_currency_symbol' ) ? get_woocommerce_currency_symbol( $code ) : $code;
			// Localized human name from WC if available.
			$name = function_exists( 'get_woocommerce_currencies' ) ? ( get_woocommerce_currencies()[ $code ] ?? $code ) : $code;

			switch ( $settings['display_format'] ) {
				case 'code':        return $code;
				case 'symbol':      return $symbol;
				case 'name':        return $name;
				case 'code_symbol':
				default:            return $symbol . ' ' . $code;
			}
		}

		/**
		 * Emoji flag for an ISO country code, derived from the currency.
		 * Returns empty string when there is no obvious mapping.
		 */
		private function flag_emoji( $currency ) {
			$map = array(
				'USD' => '🇺🇸', 'EUR' => '🇪🇺', 'GBP' => '🇬🇧', 'CAD' => '🇨🇦',
				'AUD' => '🇦🇺', 'JPY' => '🇯🇵', 'CNY' => '🇨🇳', 'INR' => '🇮🇳',
				'BRL' => '🇧🇷', 'MXN' => '🇲🇽', 'SGD' => '🇸🇬', 'AED' => '🇦🇪',
				'BDT' => '🇧🇩',
			);
			return $map[ $currency ] ?? '';
		}

		// ─────────────────────────────────────────────────────────────────
		// AJAX endpoint
		// ─────────────────────────────────────────────────────────────────

		public function ajax_switch() {
			if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), self::NONCE ) ) {
				wp_send_json_error( array( 'message' => __( 'Security check failed.', 'storzen' ) ), 403 );
			}

			$code = isset( $_POST['currency'] ) ? strtoupper( sanitize_text_field( wp_unslash( $_POST['currency'] ) ) ) : '';
			$s    = $this->get_settings();
			if ( ! $this->is_supported( $code, $s ) ) {
				wp_send_json_error( array( 'message' => __( 'Unsupported currency.', 'storzen' ) ), 400 );
			}

			// Set cookie before any output so the Set-Cookie header goes through.
			// Also tell JS the cookie name/path so it can set it client-side as
			// a belt-and-suspenders fallback (some hosts buffer AJAX output).
			$this->set_cookie( $code );
			wp_send_json_success( array(
				'currency'    => $code,
				'reload'      => true,
				'cookie_name' => self::COOKIE_KEY,
				'cookie_path' => defined( 'COOKIEPATH' ) ? COOKIEPATH : '/',
			) );
		}

	} // end class

} // end class_exists
