<?php

namespace Dobare\Core;

use DateInterval;
use DateTime;
use DateTimeZone;
use Dobare\Abstracts\Dobare_REST_Controller;
use Dobare\Utils\Dobare_Utils;
use WC_Customer;
use WC_Order;
use Exception;

class Dobare_Core
{
	private const DEFAULT_MEMBER_LABEL = 'عضویت از طریق وبسایت';

	private $sync_order_status;

	public function __construct()
	{
		add_action('wp_enqueue_scripts', array($this, 'enqueue_checkout_script'), 100);
		add_action('wp_enqueue_scripts', array($this, 'enqueue_lead_magnet_iframe_loader_script'), 999);
		add_action('wp_enqueue_scripts', array($this, 'enqueue_customer_credit_iframe_loader_script'), 999);
		add_action('woocommerce_new_customer', array($this, 'create_membership_on_new_customer'), 10, 2);
		add_action('woocommerce_checkout_update_order_review', array($this, 'apply_credit_on_checkout'), 100);
		add_action('woocommerce_checkout_update_order_review', array($this, 'persist_checkout_form_data'), 100, 1);
		add_filter('woocommerce_checkout_get_value', array($this, 'get_saved_checkout_form_data'), 100, 2);
		add_filter('woocommerce_ship_to_different_address_checked', array(
			$this,
			'get_saved_ship_to_different_address_option'
		), 100);
		add_filter('woocommerce_cart_totals_coupon_label', array($this, 'change_dobare_coupon_label'), 100, 2);
		add_filter('woocommerce_checkout_coupon_message', array($this, 'dobare_hide_coupon_messages'), 100);
		add_filter('woocommerce_coupon_error', array($this, 'dobare_hide_coupon_errors'), 10, 3);
		add_action('woocommerce_order_status_changed', array($this, 'handle_order_status_change'), 10, 4);
		add_action('woocommerce_before_calculate_totals', array($this, 'maybe_apply_credit_before_checkout'), 1);
		add_action('dobare_remove_expired_coupons_cron', array($this, 'remove_expired_dobare_discounts'));
		add_action('woocommerce_removed_coupon', array($this, 'handle_dobare_coupon_removed'), 10, 1);
		add_action('dobare_sync_customers_event', array($this, 'process_customer_sync'));
		add_action('woocommerce_before_calculate_totals', [$this, 'revalidate_gift_credit_on_cart_change'], 999);
		add_action('woocommerce_cart_updated', [$this, 'revalidate_gift_credit_on_cart_change'], 20);
		add_filter('woocommerce_coupon_is_valid_for_cart', array($this, 'control_discount_codes_with_gift_credit'), 10, 2);
		add_action('woocommerce_applied_coupon', array($this, 'handle_discount_code_applied'), 10, 1);
		add_action('template_redirect', array($this, 'check_and_apply_credit_on_navigation'), 10);
		// Render manual apply button as safety net on checkout
		add_action('woocommerce_review_order_before_submit', array($this, 'render_manual_apply_credit_button'), 20);
		add_action('woocommerce_review_order_before_order_total', array($this, 'render_suppression_notice_row'), 5);
		if (! wp_next_scheduled('dobare_remove_expired_coupons_cron')) {
			wp_schedule_event(time(), 'daily', 'dobare_remove_expired_coupons_cron');
		}

		// Add final validation hook for Dobare credit at checkout submission
		add_action('woocommerce_checkout_process', array($this, 'validate_dobare_credit_on_checkout'));

		// Add filter to log checkout AJAX responses
		add_filter('woocommerce_update_order_review_fragments', array($this, 'log_checkout_ajax_response'), 999);

		register_deactivation_hook(DOBARE_MAIN_PLUGIN_FILE, array($this, 'dobare_remove_cron_event'));
	}

	/**
	 * Build a standardized payload for Dobare member creation.
	 *
	 * @param string      $phone          Normalized phone number.
	 * @param string|null $name           Member display name.
	 * @param string|null $referral_code  Referral code if available.
	 *
	 * @return array
	 */
	public static function build_member_request_body(string $phone, ?string $name = '', ?string $referral_code = null): array
	{
		$body = array(
			'phone' => $phone,
			'name'  => (string) ($name ?? ''),
		);

		$default_label = trim(self::DEFAULT_MEMBER_LABEL);
		if ($default_label !== '') {
			$body['labels_to_add'] = array($default_label);
		}

		if ($referral_code !== null && $referral_code !== '') {
			$body['referred_by_code'] = $referral_code;
		}

		return $body;
	}

	/**
	 * Inject Dobare error notices into checkout AJAX response
	 *
	 * @param array $fragments
	 * @return array
	 */
	public function log_checkout_ajax_response($fragments)
	{
		if (Dobare_Utils::credit_perf_is_enabled()) {
			// Emit debug headers early enough for wc-ajax=update_order_review responses.
			Dobare_Utils::credit_perf_emit_headers();
			$perf_payload = Dobare_Utils::credit_perf_export();
			$fragments['dobare_credit_trace_id'] = $perf_payload['trace_id'];
			$fragments['dobare_credit_perf'] = wp_json_encode($perf_payload);
			Dobare_Utils::credit_perf_maybe_log(
				'wc-ajax=update_order_review',
				array(
					'phone' => WC()->session ? (string) WC()->session->get('dobare_credit_applied_for_phone', '') : '',
				)
			);
		}

		// Check if we have a Dobare error stored in session
		$dobare_error = WC()->session ? WC()->session->get('dobare_phone_mismatch_error') : '';
		if (!empty($dobare_error)) {
			// Generate the error notice HTML with a unique ID for JavaScript to target
			$notice_html = '<div class="woocommerce-NoticeGroup woocommerce-NoticeGroup-checkout dobare-error-notice" id="dobare-checkout-error">';
			$notice_html .= '<ul class="woocommerce-error" role="alert">';
			$notice_html .= '<li>' . esc_html($dobare_error) . '</li>';
			$notice_html .= '</ul>';
			$notice_html .= '</div>';

			// Try multiple fragment keys to ensure it gets displayed
			$fragments['.woocommerce-NoticeGroup-checkout'] = $notice_html;
			$fragments['#dobare-checkout-error'] = $notice_html;

			// Also add a custom fragment that JavaScript can detect
			$fragments['dobare_error_message'] = $dobare_error;

			// Clear the error from session after displaying
			WC()->session->set('dobare_phone_mismatch_error', '');
		}

		return $fragments;
	}

	/**
	 * Final validation for Dobare credit at checkout submission.
	 *
	 * This function performs two critical checks:
	 * 1. Internal Hash Check: Verifies that the phone number in the form has not been
	 *    tampered with after the coupon was applied.
	 * 2. External API Validation: Makes a real-time call to the Dobare API to ensure
	 *    the credit is still valid and sufficient for the purchase at the moment of payment.
	 *
	 * @return void
	 */
	public function validate_dobare_credit_on_checkout()
	{
		if (!function_exists('WC') || !WC()->cart) {
			return;
		}

		$applied_coupons = WC()->cart->get_applied_coupons();
		$dobare_coupon = null;
		$dobare_coupon_code = '';

		foreach ($applied_coupons as $coupon_code) {
			if (Dobare_Utils::is_dobare_coupon($coupon_code)) {
				$dobare_coupon = new \WC_Coupon($coupon_code);
				$dobare_coupon_code = $coupon_code;
				break;
			}
		}

		// If no Dobare coupon is applied, skip validation.
		if (!$dobare_coupon) {
			return;
		}

		$stored_hash = get_post_meta($dobare_coupon->get_id(), '_dobare_phone_hash', true);

		// If the coupon doesn't have our security hash, it's either an old or invalid coupon.
		if (empty($stored_hash)) {
			$error_message = 'اعتبار هدیه نامعتبر است. لطفاً آن را حذف کرده و دوباره اعمال کنید.';
			WC()->session->set('dobare_validation_failed', true);
			WC()->session->set('dobare_phone_mismatch_error', $error_message);
			wc_add_notice($error_message, 'error');
			return;
		}

		// 1. --- Internal Hash Check ---
		$billing_phone = isset($_POST['billing_phone']) ? sanitize_text_field(wp_unslash($_POST['billing_phone'])) : '';
		// Validate phone number format
		if (!empty($billing_phone) && !preg_match('/^[0-9+\-\s()]+$/', $billing_phone)) {
			$billing_phone = '';
		}
		$normalized_phone = Dobare_Utils::validate_and_normalize_phone_number($billing_phone);
		$current_hash = !empty($normalized_phone) ? hash('sha256', $normalized_phone) : '';

		if (empty($normalized_phone) || $current_hash !== $stored_hash) {
			WC()->session->set('dobare_validation_failed', true);
			WC()->cart->remove_coupon($dobare_coupon_code);

			$error_message = 'شماره تلفن ثبت شده در فرم با شماره تلفن اعمال شده برای اعتبار هدیه مطابقت ندارد. لطفاً شماره تلفن صحیح را وارد کرده و اعتبار را مجددا اعمال کنید.';
			WC()->session->set('dobare_phone_mismatch_error', $error_message);
			wc_add_notice($error_message, 'error');
			return;
		}

		// 2. --- External API Validation ---
		$membership = self::get_membership_uncached($normalized_phone);

		if (is_wp_error($membership)) {
			$error_message = 'خطا در اعتبارسنجی اعتبار هدیه. لطفاً دوباره تلاش کنید.';
			WC()->session->set('dobare_validation_failed', true);
			WC()->session->set('dobare_phone_mismatch_error', $error_message);
			wc_add_notice($error_message, 'error');
			return;
		}

		if (!$membership instanceof \Dobare\Core\Dobare_Membership) {
			$error_message = 'اطلاعات عضویت شما یافت نشد یا معتبر نیست. امکان استفاده از اعتبار هدیه وجود ندارد.';
			WC()->session->set('dobare_validation_failed', true);
			WC()->session->set('dobare_phone_mismatch_error', $error_message);
			wc_add_notice($error_message, 'error');
			return;
		}

		$credit_amount_to_use = (float) $dobare_coupon->get_amount();
		$available_credit = (float) $membership->gift_credit_amount;

		// Final check: ensure the customer's available credit is sufficient.
		if ($available_credit < $credit_amount_to_use) {
			$error_message = 'اعتبار هدیه شما قبلاً استفاده شده یا به پایان رسیده است و برای این خرید قابل استفاده نیست';

			WC()->cart->remove_coupon($dobare_coupon_code);
			WC()->session->set('dobare_validation_failed', true);
			WC()->session->set('dobare_phone_mismatch_error', $error_message);
			wc_add_notice($error_message, 'error');
			return;
		}
	}

	/**
	 * Build products payload per order.
	 *
	 * @param \WC_Order $order
	 * @return array
	 */
	private function build_products_payload(\WC_Order $order): array
	{
		$items_payload = array();

		$items = $order->get_items('line_item');
		if (empty($items)) {
			return $items_payload;
		}

		foreach ($items as $item) {
			if (!($item instanceof \WC_Order_Item_Product)) {
				continue;
			}

			$product = $item->get_product();
			$quantity = (float) $item->get_quantity();

			$line_total = (float) $item->get_total();
			$fee = $quantity > 0 ? (int) round($line_total / $quantity) : (int) round($line_total);

			$sku = '';
			$name = (string) $item->get_name();
			$category = null;

			if ($product) {
				$sku = (string) ($product->get_sku() ?: $product->get_id());
				$category_path = $this->get_product_category_path((int) $product->get_id());
				if (!empty($category_path)) {
					$category = $category_path;
				}
			}

			$items_payload[] = array(
				'sku'       => $sku,
				'name'      => $name,
				'category'  => $category,
				'fee'       => $fee,
				'quantity'  => $quantity,
			);
		}

		return $items_payload;
	}

	/**
	 * Return a human-readable payment method name with fallback to gateway ID.
	 *
	 * @param \WC_Order $order
	 * @return ?string
	 */
	private function get_payment_method_readable(\WC_Order $order): ?string
	{
		$title = method_exists($order, 'get_payment_method_title') ? (string) $order->get_payment_method_title() : '';
		$id    = method_exists($order, 'get_payment_method') ? (string) $order->get_payment_method() : '';
		$name  = trim($title ?: $id);

		return $name !== '' ? $name : null;
	}

	/**
	 * Build hierarchical category path for a product (e.g., "Parent > Child > Leaf").
	 *
	 * @param int $product_id
	 * @return ?string
	 */
	private function get_product_category_path(int $product_id): ?string
	{
		$terms = \get_the_terms($product_id, 'product_cat');
		if (empty($terms) || is_wp_error($terms)) {
			return null;
		}

		$best_term = null;
		$best_depth = -1;

		foreach ($terms as $term) {
			$ancestors = \get_ancestors($term->term_id, 'product_cat');
			$depth = is_array($ancestors) ? count($ancestors) : 0;
			if ($depth > $best_depth) {
				$best_depth = $depth;
				$best_term = $term;
			}
		}

		if (!$best_term) {
			return null;
		}

		$ancestor_ids = array_reverse((array) \get_ancestors($best_term->term_id, 'product_cat'));
		$names = array();
		foreach ($ancestor_ids as $aid) {
			$t = \get_term($aid, 'product_cat');
			if ($t && !is_wp_error($t)) {
				$names[] = $t->name;
			}
		}
		$names[] = $best_term->name;

		return implode(' > ', $names);
	}

	/**
	 * Build unique member labels from products categories in an order.
	 *
	 * Each node of a product category path (e.g., Parent > Child > Leaf)
	 * is converted to a member label with the pattern: --Category--{node}.
	 *
	 * @param array $products
	 * @return array
	 */
	private function build_member_labels_from_products(array $products): array
	{
		$labels = array();

		if (empty($products)) {
			return $labels;
		}

		foreach ($products as $product) {
			if (isset($product['category']) && is_string($product['category']) && $product['category'] !== '') {
				$path = (string) $product['category'];
				$nodes = preg_split('/\s*>\s*/', $path);
				if (is_array($nodes)) {
					foreach ($nodes as $node) {
						$node = trim((string) $node);
						if ($node !== '') {
							$labels[] = '--Category--' . $node;
						}
					}
				}
			}
		}

		return array_values(array_unique($labels));
	}

	/**
	 * Retrieve and cache the desired sync order status.
	 *
	 * @return string The desired order status to sync.
	 */
	private function get_sync_order_status()
	{
		if (isset($this->sync_order_status)) {
			return $this->sync_order_status;
		}
		$this->sync_order_status = Dobare_Utils::get_sync_order_status();

		return $this->sync_order_status;
	}

	/**
	 * Handle order status changes and sync with Dobare if necessary.
	 *
	 * @param int $order_id The ID of the order.
	 * @param string $old_status The old status of the order.
	 * @param string $new_status The new status of the order.
	 * @param WC_Order $order The order object.
	 */
	public function handle_order_status_change(int $order_id, string $old_status, string $new_status, WC_Order $order)
	{
		$desired_status = $this->get_sync_order_status();
		$is_child_order = $order->get_parent_id() > 0;
		if ($new_status === $desired_status && !$is_child_order) {
			$this->sync_order_with_dobare($order_id);
		}
	}

	public function remove_expired_dobare_discounts()
	{
		global $wpdb;

		$coupon_prefix = 'dobare_discount_';
		$coupon_ids = $wpdb->get_col(
			$wpdb->prepare(
				"
            SELECT ID FROM {$wpdb->posts}
            WHERE post_type = %s
            AND post_status = %s
            AND post_title LIKE %s
            ",
				'shop_coupon',
				'publish',
				$wpdb->esc_like($coupon_prefix) . '%'
			)
		);

		if (! empty($coupon_ids)) {
			$placeholders          = implode(', ', array_fill(0, count($coupon_ids), '%d'));
			$prepared_delete_query = $wpdb->prepare(
				"DELETE FROM {$wpdb->posts} WHERE ID IN ($placeholders)",
				...$coupon_ids
			);
			$wpdb->query($prepared_delete_query);

			$prepared_meta_query = $wpdb->prepare(
				"DELETE FROM {$wpdb->postmeta} WHERE post_id IN ($placeholders)",
				...$coupon_ids
			);
			$wpdb->query($prepared_meta_query);
		}
	}

	public function dobare_remove_cron_event()
	{
		wp_clear_scheduled_hook('dobare_remove_expired_coupons_cron');
	}

	public function handle_dobare_coupon_removed($coupon_code)
	{
		if (Dobare_Utils::is_dobare_coupon($coupon_code)) {
			// If this removal was triggered by our own processing, do not suppress auto-apply
			$processing_flag = function_exists('WC') && \WC()->session ? (bool) \WC()->session->get('dobare_processing_gift_credit', false) : false;

			if (! $processing_flag && function_exists('WC') && \WC()->session) {
				// User explicitly removed Dobare coupon → suppress auto re-apply until user opts in again
				Dobare_Utils::set_auto_apply_suppression(true);
				\WC()->session->set('dobare_minimum_cart_met', false);
				// Clear success notices when user removes coupon
				Dobare_Utils::remove_dobare_success_notices();
				// CRITICAL: Do NOT clear dobare_last_phone_for_applied_credit here!
				// We need to keep it to properly detect phone changes later
			}

			// Clean up the ephemeral coupon post
			$coupon_id = wc_get_coupon_id_by_code($coupon_code);
			if ($coupon_id) {
				wp_delete_post($coupon_id, true);
			}
		}
	}

	public function dobare_hide_coupon_errors($err, $err_code, $coupon)
	{
		if (is_a($coupon, 'WC_Coupon') && Dobare_Utils::is_dobare_coupon($coupon->get_code())) {
			return '';
		}

		return $err;
	}

	public function dobare_hide_coupon_messages($message)
	{
		// Hide default WooCommerce coupon messages for Dobare coupons
		// We show our own custom message instead
		if (Dobare_Utils::is_dobare_coupon($message)) {
			return '';
		}

		return $message;
	}


	function persist_checkout_form_data($posted_data)
	{
		// Avoid running in admin (e.g., theme checkout fields editor) where WC session is not available
		if (is_admin() && ! wp_doing_ajax()) {
			return;
		}

		if (! function_exists('WC')) {
			return;
		}

		$session = WC()->session ?? null;
		if (! $session || ! is_object($session) || ! method_exists($session, 'set')) {
			return;
		}

		parse_str($posted_data, $output);
		$session->set('checkout_data', $output);
	}

	function get_saved_checkout_form_data($value, $index)
	{
		// Avoid running in admin (e.g., theme checkout fields editor) where WC session is not available
		if (is_admin() && ! wp_doing_ajax()) {
			return $value;
		}

		if (! function_exists('WC')) {
			return $value;
		}

		$session = WC()->session ?? null;
		if (! $session || ! is_object($session) || ! method_exists($session, 'get')) {
			return $value;
		}

		$data = $session->get('checkout_data');
		if (! $data || empty($data[$index])) {
			return $value;
		}

		return is_bool($data[$index]) ? (int) $data[$index] : $data[$index];
	}

	function get_saved_ship_to_different_address_option($checked)
	{
		// Avoid running in admin (e.g., theme checkout fields editor) where WC session is not available
		if (is_admin() && ! wp_doing_ajax()) {
			return $checked;
		}

		if (! function_exists('WC')) {
			return $checked;
		}

		$session = WC()->session ?? null;
		if (! $session || ! is_object($session) || ! method_exists($session, 'get')) {
			return $checked;
		}

		$data = $session->get('checkout_data');
		if (! $data || empty($data['ship_to_different_address'])) {
			return $checked;
		}

		return true;
	}

	function enqueue_lead_magnet_iframe_loader_script()
	{
		wp_register_script(
			'dobare_lead_magnet_iframe_loader',
			plugin_dir_url(DOBARE_MAIN_PLUGIN_FILE) . 'build/assets/lead-magnet/js/lead-magnet-loader.js',
			array(),
			DOBARE_VERSION,
			true
		);
		// Ensure the script is non-blocking
		if (function_exists('wp_script_add_data')) {
			wp_script_add_data('dobare_lead_magnet_iframe_loader', 'defer', true);
		}

		// Determine iframe source based on custom template setting
		$use_custom = \Dobare\Utils\Dobare_Utils::is_custom_lead_magnet_enabled();
		$has_custom_content = \Dobare\Utils\Dobare_Utils::has_custom_lead_magnet('desktop') ||
			\Dobare\Utils\Dobare_Utils::has_custom_lead_magnet('mobile');

		if ($use_custom && $has_custom_content) {
			// Use REST endpoint for custom templates
			$iframe_src = rest_url('dobare/v1/lead-magnet/render');
		} else {
			// Use default static file
			$iframe_src = plugin_dir_url(DOBARE_MAIN_PLUGIN_FILE) . 'build/assets/lead-magnet/index.html';
		}

		wp_localize_script(
			'dobare_lead_magnet_iframe_loader',
			'dobareIframeData',
			array(
				'iframeSrc' => $iframe_src,
				'currency' => \Dobare\Utils\Dobare_Utils::get_currency_word(),
				'isCustom' => $use_custom && $has_custom_content,
			)
		);
		// Provide WP REST settings to avoid relying on wp-api on the frontend
		wp_localize_script(
			'dobare_lead_magnet_iframe_loader',
			'wpApiSettings',
			array(
				'root' => esc_url_raw(rest_url()),
				'nonce' => wp_create_nonce('wp_rest')
			)
		);

		wp_enqueue_script('dobare_lead_magnet_iframe_loader');
	}

	function enqueue_customer_credit_iframe_loader_script()
	{
		wp_register_script(
			'dobare_customer_credit_iframe_loader',
			plugin_dir_url(DOBARE_MAIN_PLUGIN_FILE) . 'build/assets/customer-credit/js/customer-credit-loader.js',
			array(),
			'1.0.0',
			true
		);
		// Ensure the script is non-blocking
		if (function_exists('wp_script_add_data')) {
			wp_script_add_data('dobare_customer_credit_iframe_loader', 'defer', true);
		}

		// Add WordPress API settings for customer credit script
		wp_localize_script(
			'dobare_customer_credit_iframe_loader',
			'wpApiSettings',
			array(
				'root' => esc_url_raw(rest_url()),
				'nonce' => wp_create_nonce('wp_rest')
			)
		);

		// ✅ Add iframe source data
		wp_localize_script(
			'dobare_customer_credit_iframe_loader',
			'dobareCustomerCreditData',
			array(
				'iframeSrc' => plugin_dir_url(DOBARE_MAIN_PLUGIN_FILE) . 'build/assets/customer-credit/index.html',
				'currency' => \Dobare\Utils\Dobare_Utils::get_currency_word(),
			)
		);

		wp_enqueue_script('dobare_customer_credit_iframe_loader');
	}



	function change_dobare_coupon_label($code)
	{
		if (Dobare_Utils::is_dobare_coupon($code)) {
			return 'اعتبار هدیه باشگاه مشتریان';
		}

		return $code;
	}

	function create_membership_on_new_customer(int $customer_id, WC_Customer $customer)
	{
		$this->create_membership_from_customer($customer);
	}


	public
	function create_membership_from_customer(
		WC_Customer $customer
	) {
		// Normalize phone number
		$phone = Dobare_Utils::validate_and_normalize_phone_number($customer->get_billing_phone());
		
		if (empty($phone)) {
			return;
		}
		
		// Prepare member data using new API format
		$name = trim($customer->get_first_name() . ' ' . $customer->get_last_name());
		$referral_code_value = null;
		// Check if customer has a referral code from EnterX
		$user_id = $customer->get_id();
		if ($user_id) {
			$referral_code = get_user_meta($user_id, 'enterx_referral_code', true);
			if (!empty($referral_code)) {
				$referral_code_value = sanitize_text_field($referral_code);
			}
		}

		$body = self::build_member_request_body($phone, $name, $referral_code_value);
		
		// Get API key
		$api_key = Dobare_Utils::get_dob_api_key();
		if (empty($api_key)) {
			return;
		}
		
		// Use new v1/members/ endpoint instead of legacy v1/integrations/memberships/
		Dobare_REST_Controller::dobare_remote_post('v1/members/', $body, array('API-KEY' => $api_key));
	}

	public
	function sync_order_with_dobare(
		$order_id
	) {
		$order = wc_get_order($order_id);
		if (! empty($order)) {
			$total_used_gift = 0;
			$external_discount_code = null;
			$external_discount_amount = null;
			$coupons = $order->get_coupons();
			foreach ($coupons as $coupon) {
				// Sum Dobare gift credit amount
				if (Dobare_Utils::is_dobare_coupon($coupon->get_code())) {
					$total_used_gift = $coupon->get_discount();
					continue;
				}
				// Capture the first non-Dobare coupon as external discount
				if ($external_discount_code === null) {
					$external_discount_code = (string) $coupon->get_code();
					$external_discount_amount = (int) round((float) $coupon->get_discount());
				}
			}

			$member_phone = \Dobare\Utils\Dobare_Utils::validate_and_normalize_phone_number((string) $order->get_billing_phone());
			$member_name  = trim(($order->get_billing_first_name() ?? '') . ' ' . ($order->get_billing_last_name() ?? ''));

			$body    = array(
				"source_order_id"            => (string) $order->get_id(),
				"member_name"                => $member_name,
				"member_phone"               => (string) $member_phone,
				"total_paid_amount"          => (int) round((float) $order->get_total()),
				"dobare_gift_credit_amount"  => (int) $total_used_gift,
			);

			// New fields: include first external discount code and amount if present
			if (!empty($external_discount_code)) {
				$body['discount_code'] = $external_discount_code;
			}
			if (!is_null($external_discount_amount)) {
				$body['discount_amount'] = (int) $external_discount_amount;
			}

			$products = $this->build_products_payload($order);
			if (!empty($products)) {
				$body['products'] = $products;
				// Derive member labels from category nodes
				$category_labels = $this->build_member_labels_from_products($products);
				if (!empty($category_labels)) {
					$body['member_labels_to_add'] = $category_labels;
				}
			}

			$payment_method = $this->get_payment_method_readable($order);
			if (!empty($payment_method)) {
				$body['payment_method'] = $payment_method;
			}
			$api_key = Dobare_Utils::get_dob_api_key();
			if (! empty($api_key)) {
				$response = Dobare_REST_Controller::dobare_remote_post('v1/orders/', $body, array('API-KEY' => $api_key));
			}
			foreach ($coupons as $coupon) {
				if (Dobare_Utils::is_dobare_coupon($coupon->get_code())) {
					$coupon->delete();
				}
			}
		}
	}


	public function maybe_apply_credit_before_checkout()
	{
		// Skip AJAX requests but allow cart page processing
		if (is_ajax()) {
			return;
		}

		if (\Dobare\Utils\Dobare_Utils::is_credit_temporarily_disabled()) {
			\Dobare\Utils\Dobare_Utils::remove_dobare_coupons();
			if (function_exists('WC') && \WC()->session) {
				\WC()->session->set('dobare_minimum_cart_met', false);
			}
			\Dobare\Utils\Dobare_Utils::remove_dobare_success_notices();
			return;
		}


		$applied_coupons       = WC()->cart->get_applied_coupons();
		$dobare_coupon_applied = false;
		foreach ($applied_coupons as $coupon_code) {
			if (Dobare_Utils::is_dobare_coupon($coupon_code)) {
				$dobare_coupon_applied = true;
				break;
			}
		}

		// Infer a normalized phone similar to revalidation flow
		$phone_to_evaluate = WC()->session->get('dobare_credit_applied_for_phone', '');
		if (empty($phone_to_evaluate)) {
			$inferred_phone = '';
			$customer = WC()->customer;
			if ($customer) {
				$inferred_phone = $customer->get_billing_phone() ?: $customer->get_shipping_phone();
			}
			if (empty($inferred_phone)) {
				$inferred_phone = WC()->session->get('billing_phone');
			}
			if (empty($inferred_phone)) {
				$inferred_phone = WC()->session->get('shipping_phone');
			}
			if (empty($inferred_phone)) {
				$checkout_data = WC()->session->get('checkout_data');
				if (!empty($checkout_data)) {
					$inferred_phone = $checkout_data['billing_phone'] ?? ($checkout_data['shipping_phone'] ?? '');
				}
			}
			$inferred_phone = \Dobare\Utils\Dobare_Utils::validate_and_normalize_phone_number((string) $inferred_phone);

			if (!empty($inferred_phone)) {
				$phone_to_evaluate = $inferred_phone;
				WC()->session->set('dobare_credit_applied_for_phone', $phone_to_evaluate);
			}
		}

		if (is_checkout() || is_cart()) {
			if (!$dobare_coupon_applied && empty($phone_to_evaluate)) {
				// Show phone prompt once; de-duplication handled in util
				Dobare_Utils::add_phone_prompt_notice();
			} else {
				// Ensure we remove any stale phone prompts when phone exists or coupon applied
				Dobare_Utils::remove_dobare_phone_prompt_notices();
			}
		}
	}

	/**
	 * Central method to revalidate gift credit when cart changes
	 *
	 * This method handles reapplying or removing gift credit based on current cart state
	 * and customer phone number. It includes loop prevention for cart calculation events.
	 */
	/**
	 * Check and apply gift credit when user navigates to cart or checkout pages
	 * This handles cases where phone number is already stored but credit wasn't applied
	 */
	public function check_and_apply_credit_on_navigation(): void
	{
		// Only run on cart or checkout pages
		if (!is_cart() && !is_checkout()) {
			return;
		}

		// Delegate to unified revalidation flow to avoid duplication
		$this->revalidate_gift_credit_on_cart_change();
	}

	public function revalidate_gift_credit_on_cart_change()
	{
		if (!WC()->cart || !WC()->session) {
			return;
		}

		$timer_total = 0.0;
		if (Dobare_Utils::credit_perf_is_enabled()) {
			$timer_total = Dobare_Utils::credit_perf_start('revalidate_gift_credit_on_cart_change');
		}

		if (\Dobare\Utils\Dobare_Utils::is_credit_temporarily_disabled()) {
			\Dobare\Utils\Dobare_Utils::remove_dobare_coupons();
			WC()->session->set('dobare_minimum_cart_met', false);
			\Dobare\Utils\Dobare_Utils::remove_dobare_success_notices();
			if (Dobare_Utils::credit_perf_is_enabled()) {
				Dobare_Utils::credit_perf_end('revalidate_gift_credit_on_cart_change', (float) $timer_total, array('result' => 'credit_disabled'));
			}
			return;
		}

		$is_ajax_request = (defined('DOING_AJAX') && DOING_AJAX) || wp_doing_ajax();
		// Check if gift credit is already being processed to prevent infinite loops
		$processing_flag = WC()->session->get('dobare_processing_gift_credit', false);
		if ($processing_flag) {
			if (Dobare_Utils::credit_perf_is_enabled()) {
				Dobare_Utils::credit_perf_end('revalidate_gift_credit_on_cart_change', (float) $timer_total, array('result' => 'processing_flag'));
			}
			return; // Exit early if already processing to prevent infinite loops
		}

		$cart_subtotal = WC()->cart->get_subtotal();
		$cart_is_empty = WC()->cart->is_empty();
		$minimum = Dobare_Utils::get_minimum_cart_total_for_credit();
		$dobare_minimum_cart_met = WC()->session->get('dobare_minimum_cart_met', false);
		$phone_to_evaluate = WC()->session->get('dobare_credit_applied_for_phone', '');
		$cart_items_count = WC()->cart->get_cart_contents_count();
		$manual_apply_until = (int) WC()->session->get('dobare_manual_apply_credit_until', 0);
		$manual_apply_requested = $manual_apply_until > time();
		if (!$manual_apply_requested && $manual_apply_until > 0) {
			WC()->session->set('dobare_manual_apply_credit_until', 0);
		}

		$subtotal_unknown_during_ajax = $is_ajax_request && $cart_items_count > 0 && floatval($cart_subtotal) <= 0;
		if ($subtotal_unknown_during_ajax) {
			if (Dobare_Utils::credit_perf_is_enabled()) {
				Dobare_Utils::credit_perf_end('revalidate_gift_credit_on_cart_change', (float) $timer_total, array('result' => 'subtotal_unknown_ajax'));
			}
			return;
		}

		// If we don't yet have a phone in session, try to infer it from customer/session data
		if (empty($phone_to_evaluate)) {
			$inferred_phone = '';
			$customer = WC()->customer;
			if ($customer) {
				$inferred_phone = $customer->get_billing_phone() ?: $customer->get_shipping_phone();
			}
			if (empty($inferred_phone)) {
				$inferred_phone = WC()->session->get('billing_phone');
			}
			if (empty($inferred_phone)) {
				$inferred_phone = WC()->session->get('shipping_phone');
			}
			if (empty($inferred_phone)) {
				$checkout_data = WC()->session->get('checkout_data');
				if (!empty($checkout_data)) {
					$inferred_phone = $checkout_data['billing_phone'] ?? ($checkout_data['shipping_phone'] ?? '');
				}
			}
			$inferred_phone = \Dobare\Utils\Dobare_Utils::validate_and_normalize_phone_number((string) $inferred_phone);
			if (!empty($inferred_phone)) {
				$phone_to_evaluate = $inferred_phone;
				WC()->session->set('dobare_credit_applied_for_phone', $phone_to_evaluate);
			}
		}

		// CRITICAL FIX: Check if there's currently NO Dobare coupon but suppression is active
		// This handles page refresh scenarios where session persists but coupon is gone
		$has_dobare_coupon = false;
		$applied_coupons = WC()->cart->get_applied_coupons();
		foreach ($applied_coupons as $coupon_code) {
			if (Dobare_Utils::is_dobare_coupon($coupon_code)) {
				$has_dobare_coupon = true;
				break;
			}
		}

		$auto_suppressed = (bool) WC()->session->get('dobare_suppress_auto_apply', false);

		// CRITICAL FIX: Only clear success notices if NO coupon is applied
		// If a coupon IS applied, keep the success notice (it was just added by apply_gift_credit)
		// This prevents removing the notice we just added
		if (!$has_dobare_coupon) {
			Dobare_Utils::remove_dobare_success_notices();
		}

		// Only clear suppression on PAGE REFRESH, not during AJAX (same session)
		// If suppression is active but no coupon exists, check if this is a page navigation
		if ($cart_subtotal <= 0 && !$cart_is_empty) {
			if (Dobare_Utils::credit_perf_is_enabled()) {
				Dobare_Utils::credit_perf_end('revalidate_gift_credit_on_cart_change', (float) $timer_total, array('result' => 'subtotal_nonpositive'));
			}
			return;
		}

		if ($auto_suppressed && !$has_dobare_coupon && !empty($phone_to_evaluate) && !$manual_apply_requested) {
			// Respect the customer's decision until they manually re-enable credit.
			if (Dobare_Utils::credit_perf_is_enabled()) {
				Dobare_Utils::credit_perf_end('revalidate_gift_credit_on_cart_change', (float) $timer_total, array('result' => 'suppressed'));
			}
			return;
		}

		if ($cart_subtotal < $minimum) {
			// Ensure coupons removed & flag false
			Dobare_Utils::remove_dobare_coupons();
			WC()->session->set('dobare_minimum_cart_met', false);

			// Check if we should show minimum cart notice
			if ($minimum > 0 && !empty($phone_to_evaluate)) {
				// Get membership to check if user has credit
				$membership = Dobare_Core::get_membership($phone_to_evaluate);
				if ($membership instanceof Dobare_Membership && $membership->gift_credit_amount > 0) {
					// User has credit but cart doesn't meet minimum - show notice
					Dobare_Utils::add_minimum_cart_notice($minimum, $cart_subtotal);
				}
			}
		} elseif (!$dobare_minimum_cart_met || $manual_apply_requested) {
			// Call apply_gift_credit() to add coupon since flag is false
			if (!empty($phone_to_evaluate)) {
				Dobare_Utils::apply_gift_credit($phone_to_evaluate);
			}
			// If this was a manual apply request, clear the one-shot flag after attempting to apply.
			if ($manual_apply_requested) {
				WC()->session->set('dobare_manual_apply_credit_until', 0);
			}
		}

		// If subtotal is now >= minimum, ensure stale minimum threshold notices are gone
		if ($cart_subtotal >= $minimum) {
			Dobare_Utils::remove_dobare_minimum_cart_notices();
		}

		if (Dobare_Utils::credit_perf_is_enabled()) {
			Dobare_Utils::credit_perf_end('revalidate_gift_credit_on_cart_change', (float) $timer_total, array('result' => 'ok'));
		}

		// Scenario where subtotal >= minimum & flag is true, nothing extra is needed
	}

	/**
	 * Control discount codes application when credit with discount feature is enabled
	 *
	 * This method prevents multiple discount codes from being applied at the same time
	 * while allowing gift credits to work alongside one discount code
	 *
	 * @param bool $is_valid Whether the coupon is valid for the cart
	 * @param WC_Coupon $coupon The coupon object being validated
	 * @return bool Whether the coupon is valid
	 */
	public function control_discount_codes_with_gift_credit($is_valid, $coupon)
	{
		// Only apply this logic if the feature is enabled
		if (!Dobare_Utils::is_credit_with_discount_enabled()) {
			return $is_valid;
		}

		// Skip validation for Dobare gift credit coupons
		if (Dobare_Utils::is_dobare_coupon($coupon->get_code())) {
			return $is_valid;
		}

		// If this is a regular discount code, check if there are already other discount codes applied
		$applied_coupons = $this->get_applied_coupons_from_context();

		if ($applied_coupons === false) {
			return false;
		}
		$current_coupon_code = $coupon->get_code();
		$non_dobare_coupons = array();

		foreach ($applied_coupons as $applied_coupon_code) {
			// Skip the current coupon being validated and Dobare coupons
			if ($applied_coupon_code !== $current_coupon_code && !Dobare_Utils::is_dobare_coupon($applied_coupon_code)) {
				$non_dobare_coupons[] = $applied_coupon_code;
			}
		}

		// If there's already a non-Dobare discount code applied (excluding the current one), prevent this one
		if (count($non_dobare_coupons) > 0) {
			// Add a notice to inform the customer
			wc_add_notice(
				'شما فقط می‌توانید یک کد تخفیف در هر سفارش استفاده کنید. کد تخفیف قبلی را حذف کنید تا کد جدید را اعمال کنید.',
				'error'
			);
			return false;
		}

		return $is_valid;
	}

	/**
	 * Get applied coupons from available context (cart or order)
	 *
	 * @return array|false Array of applied coupon codes or false if unavailable
	 */
	private function get_applied_coupons_from_context()
	{
		// First try to get from cart (frontend context)
		if (WC()->cart && is_object(WC()->cart)) {
			return WC()->cart->get_applied_coupons();
		}

		// If cart is not available, try to get from current order being processed
		// This handles admin context during order recalculation
		$order = $this->get_current_order_context();
		if ($order && is_a($order, 'WC_Order')) {
			return $order->get_coupon_codes();
		}

		// If we're in admin and have a POST request with order_id
		if (is_admin() && isset($_POST['order_id'])) {
			$order_id = absint(wp_unslash($_POST['order_id']));
			$order = wc_get_order(intval($order_id));
			if ($order && is_a($order, 'WC_Order')) {
				return $order->get_coupon_codes();
			}
		}

		// Check for order ID in various global contexts
		global $post;

		if ($post && isset($post->ID) && get_post_type($post->ID) === 'shop_order') {
			$order = wc_get_order($post->ID);
			if ($order && is_a($order, 'WC_Order')) {
				return $order->get_coupon_codes();
			}
		}

		// Last resort: check if there's an order in the current screen context
		if (is_admin()) {
			$screen = get_current_screen();
			if ($screen && $screen->id === 'shop_order' && isset($_GET['post']) && is_numeric($_GET['post'])) {
				$order_id = sanitize_text_field(wp_unslash($_GET['post']));
				$order = wc_get_order(intval($order_id));
				if ($order && is_a($order, 'WC_Order')) {
					return $order->get_coupon_codes();
				}
			}
		}

		// If all methods fail, return false to indicate we couldn't get the data
		return false;
	}

	/**
	 * Try to get the current order from various contexts
	 *
	 * @return WC_Order|false The current order or false if not found
	 */
	private function get_current_order_context()
	{
		// Try to get order from WooCommerce globals or context
		if (function_exists('wc_get_order') && isset($GLOBALS['wc_order_being_recalculated'])) {
			return $GLOBALS['wc_order_being_recalculated'];
		}

		// Check current filter context for order hints
		global $wp_current_filter;
		if (is_array($wp_current_filter)) {
			foreach ($wp_current_filter as $filter) {
				if (strpos($filter, 'order') !== false || strpos($filter, 'woocommerce_process') !== false) {
					// We might be in an order context, but we need more specific detection
					break;
				}
			}
		}

		return false;
	}

	/**
	 * Handle when a discount code is applied to remove conflicting coupons
	 *
	 * This method ensures that when a new discount code is applied,
	 * any existing discount codes are removed (except gift credits)
	 *
	 * @param string $coupon_code The coupon code that was applied
	 */
	public function handle_discount_code_applied($coupon_code)
	{
		// Only apply this logic if the feature is enabled
		if (!Dobare_Utils::is_credit_with_discount_enabled()) {
			return;
		}

		// Skip processing for Dobare gift credit coupons
		if (Dobare_Utils::is_dobare_coupon($coupon_code)) {
			return;
		}

		// Get all applied coupons
		$applied_coupons = WC()->cart->get_applied_coupons();

		// Remove other non-Dobare discount codes (keep only the new one and gift credits)
		foreach ($applied_coupons as $applied_coupon_code) {
			if ($applied_coupon_code !== $coupon_code && !Dobare_Utils::is_dobare_coupon($applied_coupon_code)) {
				WC()->cart->remove_coupon($applied_coupon_code);
			}
		}
	}

	/**
	 * Process a batch of customers for sync
	 */
	public function process_customer_sync()
	{
		try {
			// Set unlimited execution time
			set_time_limit(0);

			\Dobare\Utils\Dobare_Utils::set_sync_customers_progress('local_processing', 0, 0, null, 'Starting customer data collection...');

			// Create a temp file to store customer data
			$upload_dir = \wp_upload_dir();
			$file_path = $upload_dir['basedir'] . '/dobare-customers-' . uniqid() . '.csv';
			$file = @\fopen($file_path, 'w');

			if (!$file) {
				throw new \Exception('Failed to create temporary CSV file at: ' . $file_path);
			}

			// Write CSV header
			\fputcsv($file, ['phone', 'first_name', 'last_name', 'customer_id']);
			\fclose($file);

			// Process orders in batches for memory efficiency
			$chunk_size = 50;
			$offset = 0;
			$processed = 0;
			$unique_customers = 0;
			$processed_phones = array();

			// Update progress
			\Dobare\Utils\Dobare_Utils::set_sync_customers_progress('local_processing', 5, 0, null, 'Starting to process orders...');

			// Track start time for timeout detection
			$process_start_time = \time();
			$max_execution_time = 2700; // 45 minutes (leave some buffer before the PHP limit)

			// Process orders in batches
			do {
				// Check if we should timeout
				if (\time() - $process_start_time > $max_execution_time) {
					@\unlink($file_path);
					throw new \Exception('Process timed out after ' . $max_execution_time . ' seconds');
				}

				// Calculate progress percentage
				$current_progress = min(90, 5 + ($offset / ($offset + 100) * 85));

				// Update progress
				\Dobare\Utils\Dobare_Utils::set_sync_customers_progress(
					'local_processing',
					$current_progress,
					0,
					null,
					sprintf('Processing orders batch at offset %d, found %d unique customers', $offset, $unique_customers)
				);

				// Get a chunk of orders using IDs only
				$order_ids = \wc_get_orders(array(
					'limit'      => $chunk_size,
					'offset'     => $offset,
					'return'     => 'ids',
					'status'     => 'any',
					'type'       => 'shop_order', // Explicitly request only shop_order
				));

				$batch_count = count($order_ids);
				if (empty($order_ids)) {
					break;
				}

				// Open the file in append mode for each batch
				$file = @\fopen($file_path, 'a');
				if (!$file) {
					throw new \Exception('Failed to open CSV file for appending: ' . $file_path);
				}

				// Process each order in the batch
				foreach ($order_ids as $order_id) {
					try {
						// Get only the order data we need, not the full order object
						$order = \wc_get_order($order_id);

						// Skip invalid orders
						if (!($order instanceof \WC_Order) || $order instanceof \WC_Order_Refund || !method_exists($order, 'get_billing_phone')) {
							continue;
						}

						// Extract only the data we need
						$phone = $order->get_billing_phone();
						$phone = preg_replace('/[^0-9+]/', '', $phone);
						$normalized_phone = \Dobare\Utils\Dobare_Utils::validate_and_normalize_phone_number($phone);

						// Skip if empty or already processed
						if (empty($normalized_phone) || isset($processed_phones[$normalized_phone])) {
							continue;
						}

						// Add to processed phones
						$processed_phones[$normalized_phone] = true;
						$unique_customers++;

						// Write this customer to CSV
						\fputcsv($file, array(
							$normalized_phone,
							$order->get_billing_first_name() ?? '',
							$order->get_billing_last_name() ?? '',
							$order->get_customer_id() ?? ''
						));

						// Free memory
						unset($order);
					} catch (\Exception $order_ex) {
						\error_log('Dobare customer sync: Error processing order ' . $order_id . ': ' . $order_ex->getMessage());
						// Continue with next order
					}
				}

				// Close file after each batch to free file handle resources
				\fclose($file);

				// Free memory
				unset($order_ids);

				// Force garbage collection
				if (function_exists('gc_collect_cycles')) {
					gc_collect_cycles();
				}

				// Move to next batch
				$offset += $chunk_size;
				$processed += $batch_count;

				// Sleep briefly to allow other processes to run
				\usleep(100000); // 100ms
			} while ($batch_count > 0);

			// Check if we have any customers to upload
			if ($unique_customers === 0) {
				\Dobare\Utils\Dobare_Utils::set_sync_customers_progress('completed', 100, 100, null, 'No customers with valid phone numbers found');
				@\unlink($file_path);
				return;
			}

			// Update progress before uploading
			\Dobare\Utils\Dobare_Utils::set_sync_customers_progress('local_processing', 95, 0, null, 'CSV file generated with ' . $unique_customers . ' customers. Preparing to upload...');

			// Upload the CSV file to the Dobare backend
			try {
				// Upload file in a separate process if possible
				\Dobare\Utils\Dobare_Utils::set_sync_customers_progress('uploading', 100, 5, null, 'Uploading customer data to Dobare...');

				$upload_result = $this->upload_customers_csv_to_dobare($file_path);

				// Check upload result
				if (isset($upload_result['success']) && $upload_result['success']) {
					// Check for remote_id in the response
					$remote_id = isset($upload_result['remote_id']) ? $upload_result['remote_id'] : null;

					if (!empty($remote_id)) {
						// If we have a remote ID, we're now in remote processing phase
						\Dobare\Utils\Dobare_Utils::set_sync_customers_progress(
							'remote_processing',
							100,   // Local phase complete
							10,    // Remote phase just started
							$remote_id,
							'Data uploaded successfully, awaiting remote processing...'
						);

						// Schedule a check for remote status
						wp_schedule_single_event(time() + 30, 'dobare_check_remote_sync_status');
					} else {
						// No remote ID but still successful (older API version)
						\Dobare\Utils\Dobare_Utils::set_sync_customers_progress(
							'completed',
							100,
							100,
							null,
							'Customer sync completed successfully! Uploaded ' . $unique_customers . ' customers.'
						);
					}
				} else {
					$error_message = isset($upload_result['message']) ? $upload_result['message'] : 'Unknown error during upload';
					\Dobare\Utils\Dobare_Utils::set_sync_customers_progress('error', 95, 0, null, 'Error uploading CSV: ' . $error_message);
				}
			} catch (\Exception $upload_ex) {
				\Dobare\Utils\Dobare_Utils::set_sync_customers_progress('error', 95, 0, null, 'Exception during upload: ' . $upload_ex->getMessage());
			} finally {
				// Clean up the file
				if (file_exists($file_path)) {
					@\unlink($file_path);
				}
			}

			// Log completion
			\error_log('Dobare customer sync: Process completed at ' . \date('Y-m-d H:i:s'));
		} catch (\Exception $e) {
			// Log the error and update progress
			\error_log('Dobare customer sync critical error: ' . $e->getMessage());
			\Dobare\Utils\Dobare_Utils::set_sync_customers_progress('error', 0, 0, null, 'Sync process failed: ' . $e->getMessage());

			// Clear the scheduled event to prevent retries of failed process
			\wp_clear_scheduled_hook('dobare_sync_customers_event');
		}
	}

	/**
	 * Create a coupon for a membership
	 *
	 * @param Dobare_Membership $membership
	 * @return string
	 */
	public static function create_coupon_for_membership(Dobare_Membership $membership): string
	{
		$timer_total = 0.0;
		if (Dobare_Utils::credit_perf_is_enabled()) {
			$timer_total = Dobare_Utils::credit_perf_start('create_coupon_for_membership_internal');
		}

		$coupon_code = Dobare_Utils::generate_random_coupon_code();
		$post        = array(
			'post_title'   => $coupon_code,
			'post_content' => "تخفیف مشتری در باشگاه مشتریان",
			'post_status'  => 'publish',
			'post_type'    => 'shop_coupon',
			'post_author'  => 1,
		);

		$timer_insert = 0.0;
		if (Dobare_Utils::credit_perf_is_enabled()) {
			$timer_insert = Dobare_Utils::credit_perf_start('coupon_wp_insert_post');
		}

		$post_id = wp_insert_post($post);

		if (Dobare_Utils::credit_perf_is_enabled()) {
			Dobare_Utils::credit_perf_end('coupon_wp_insert_post', (float) $timer_insert, array('post_id' => (int) $post_id));
		}

		if (! $post_id) {
			if (Dobare_Utils::credit_perf_is_enabled()) {
				Dobare_Utils::credit_perf_end('create_coupon_for_membership_internal', (float) $timer_total, array('result' => 'insert_failed'));
			}
			return '';
		}

		$timer_meta = 0.0;
		if (Dobare_Utils::credit_perf_is_enabled()) {
			$timer_meta = Dobare_Utils::credit_perf_start('coupon_meta_updates');
		}
		$currentDateTime = new DateTime('now', new DateTimeZone('Asia/Tehran'));
		$currentDateTime->add(new DateInterval("PT" . '5' . "M"));
		$couponExpiryDateTime       = $currentDateTime;
		$customer_email             = "";
		$type                       = 'fixed_cart';
		$amount                     = wc_format_decimal($membership->gift_credit_amount);
		$usage_limit                = 1;
		$usage_limit_per_user       = 1;
		$expiry_date                = $couponExpiryDateTime->getTimestamp();
		$apply_before_tax           = 'no';
		$exclude_sale_items         = 'no';
		$show_on_store              = 'yes';
		$free_shipping              = 'no';
		$apply_new_products         = 'yes';
		$minimum_amount             = wc_format_decimal("0");
		$product_ids                = '';
		$exclude_product_ids        = '';
		$product_categories         = array();
		$exclude_product_categories = array();
		$enabled_for_vendor         = 'yes';
		$shared_coupon_amount       = '';
		$notify_to_vendors          = 'yes';
		$show_on_stores             = 'yes';
		$shared_coupon_type         = 'percentage';
		$vendors_ids                = '';
		$exclude_vendors_ids        = '';
		$coupon_commissions_type    = 'from_admin';
		update_post_meta($post_id, 'discount_type', $type);
		update_post_meta($post_id, 'coupon_commissions_type', $coupon_commissions_type);
		update_post_meta($post_id, 'coupon_amount', $amount);
		update_post_meta($post_id, 'product_ids', $product_ids);
		update_post_meta($post_id, 'exclude_product_ids', $exclude_product_ids);
		update_post_meta($post_id, 'product_categories', $product_categories);
		update_post_meta($post_id, 'exclude_product_categories', $exclude_product_categories);
		update_post_meta($post_id, 'usage_limit', $usage_limit);
		update_post_meta($post_id, 'usage_limit_per_user', $usage_limit_per_user);
		update_post_meta($post_id, 'date_expires', $expiry_date);
		update_post_meta($post_id, 'apply_before_tax', $apply_before_tax);
		update_post_meta($post_id, 'exclude_sale_items', $exclude_sale_items);
		update_post_meta($post_id, 'apply_new_products', $apply_new_products);
		update_post_meta($post_id, 'show_on_store', $show_on_store);
		update_post_meta($post_id, 'free_shipping', $free_shipping);
		update_post_meta($post_id, 'minimum_amount', $minimum_amount);
		update_post_meta($post_id, 'customer_email', $customer_email);
		update_post_meta($post_id, 'dokan_admin_coupon', 'yes');
		do_action('dokan_after_coupon_create', $post_id);
		do_action(
			'dokan_admin_coupon_options_before_save',
			$post_id,
			[
				'admin_coupons_enabled_for_vendor'     => $enabled_for_vendor,
				'coupon_commissions_type'              => $coupon_commissions_type,
				'coupons_vendors_ids'                  => $vendors_ids,
				'coupons_exclude_vendors_ids'          => $exclude_vendors_ids,
				'admin_shared_coupon_amount'           => $shared_coupon_amount,
				'admin_shared_coupon_type'             => $shared_coupon_type,
				'admin_coupons_send_notify_to_vendors' => $notify_to_vendors,
				'admin_coupons_show_on_stores'         => $show_on_stores,
			]
		);
		update_post_meta($post_id, 'admin_coupons_enabled_for_vendor', $enabled_for_vendor);
		update_post_meta($post_id, 'coupon_commissions_type', $coupon_commissions_type);
		update_post_meta($post_id, 'coupons_vendors_ids', $vendors_ids);
		update_post_meta($post_id, 'coupons_exclude_vendors_ids', $exclude_vendors_ids);
		update_post_meta($post_id, 'admin_shared_coupon_amount', $shared_coupon_amount);
		update_post_meta($post_id, 'admin_shared_coupon_type', $shared_coupon_type);
		update_post_meta($post_id, 'admin_coupons_send_notify_to_vendors', $notify_to_vendors);
		update_post_meta($post_id, 'admin_coupons_show_on_stores', $show_on_stores);

		// Store a hash of the phone number for security validation at checkout
		if (isset($membership->phone) && !empty($membership->phone)) {
			update_post_meta($post_id, '_dobare_phone_hash', hash('sha256', $membership->phone));
		}

		do_action('dokan_admin_coupon_options_save', $post_id, $coupon_code);

		if (Dobare_Utils::credit_perf_is_enabled()) {
			Dobare_Utils::credit_perf_end('coupon_meta_updates', (float) $timer_meta);
			Dobare_Utils::credit_perf_end('create_coupon_for_membership_internal', (float) $timer_total, array('result' => 'ok'));
		}

		return $coupon_code;
	}

	/**
	 * Extract core member fields from Dobare API payload.
	 *
	 * @param array $data
	 * @return array{
	 *   name: string,
	 *   phone: string,
	 *   gift_credit: int
	 * }
	 */
	private static function extract_member_core_fields(array $data): array
	{
		return array(
			'name'        => (string) ($data['name'] ?? ''),
			'phone'       => (string) ($data['phone'] ?? ''),
			'gift_credit' => (int) ($data['gift_credit'] ?? 0),
		);
	}

	/**
	 * Fetch membership from Dobare API without using any caches.
	 *
	 * @param string $phone Normalized phone number.
	 * @param string $perf_event Perf label for tracing.
	 * @return Dobare_Membership|\WP_Error|string
	 */
	private static function fetch_membership_from_api(string $phone, string $perf_event = 'dobare_api_get_member')
	{
		$api_key = Dobare_Utils::get_dob_api_key();
		if (empty($api_key)) {
			return '';
		}

		$timer = 0.0;
		if (Dobare_Utils::credit_perf_is_enabled()) {
			$timer = Dobare_Utils::credit_perf_start($perf_event);
		}

		$response = Dobare_REST_Controller::dobare_remote_get('v1/members/' . $phone . '/', array('API-KEY' => $api_key));

		if (Dobare_Utils::credit_perf_is_enabled()) {
			$status_context = array(
				'status' => is_wp_error($response) ? 'wp_error' : (int) ($response['status'] ?? 0),
			);
			Dobare_Utils::credit_perf_end($perf_event, (float) $timer, $status_context);
		}

		if (is_wp_error($response) || (int) ($response['status'] ?? 0) !== 200) {
			return $response;
		}

		$raw_member_data = isset($response['data']['data']) && is_array($response['data']['data']) ? $response['data']['data'] : array();
		$core_fields = self::extract_member_core_fields($raw_member_data);

		return new Dobare_Membership($core_fields['name'], $core_fields['phone'], $core_fields['gift_credit']);
	}

	/**
	 * Get the session-prefetch TTL for membership (in seconds).
	 *
	 * @param string $phone Normalized phone number.
	 * @return int
	 */
	private static function get_membership_prefetch_ttl_seconds(string $phone): int
	{
		$ttl = (int) apply_filters('dobare_membership_prefetch_ttl', 600, $phone);

		// Keep it reasonable: at least 30s, at most 1 hour by default.
		$ttl = max(30, $ttl);
		$ttl = min(3600, $ttl);

		return $ttl;
	}

	/**
	 * Read a membership snapshot from WooCommerce session prefetch cache.
	 *
	 * @param string $phone Normalized phone number.
	 * @return Dobare_Membership|null
	 */
	private static function get_prefetched_membership_from_session(string $phone): ?Dobare_Membership
	{
		if (!function_exists('WC') || !WC()->session) {
			return null;
		}

		$session = WC()->session;
		$cache = $session->get('dobare_membership_prefetch', array());
		if (!is_array($cache) || empty($cache[$phone]) || !is_array($cache[$phone])) {
			return null;
		}

		$entry = $cache[$phone];
		$fetched_at = isset($entry['fetched_at']) ? (int) $entry['fetched_at'] : 0;
		$ttl = self::get_membership_prefetch_ttl_seconds($phone);
		if ($fetched_at <= 0 || (time() - $fetched_at) > $ttl) {
			// Expired: remove it to keep the session clean.
			unset($cache[$phone]);
			$session->set('dobare_membership_prefetch', $cache);
			return null;
		}

		$name = isset($entry['name']) ? (string) $entry['name'] : '';
		$cached_phone = isset($entry['phone']) ? (string) $entry['phone'] : $phone;
		$gift_credit_amount = isset($entry['gift_credit_amount']) ? (int) $entry['gift_credit_amount'] : 0;

		return new Dobare_Membership($name, $cached_phone, $gift_credit_amount);
	}

	/**
	 * Store a membership snapshot into WooCommerce session prefetch cache.
	 *
	 * @param Dobare_Membership $membership
	 * @return void
	 */
	private static function store_prefetched_membership_in_session(Dobare_Membership $membership): void
	{
		if (!function_exists('WC') || !WC()->session) {
			return;
		}

		$session = WC()->session;
		$phone = trim((string) $membership->phone);
		if ($phone === '') {
			return;
		}

		$cache = $session->get('dobare_membership_prefetch', array());
		if (!is_array($cache)) {
			$cache = array();
		}

		$cache[$phone] = array(
			'phone'              => $phone,
			'name'               => (string) $membership->name,
			'gift_credit_amount' => (int) $membership->gift_credit_amount,
			'fetched_at'         => time(),
		);

		$session->set('dobare_membership_prefetch', $cache);
	}

	/**
	 * Prefetch membership into WooCommerce session (one-shot per TTL, with in-flight guard).
	 *
	 * @param string $phone Normalized phone number.
	 * @param string $reason Optional reason for diagnostics.
	 * @return void
	 */
	private static function prefetch_membership_for_session(string $phone, string $reason = ''): void
	{
		if (!function_exists('WC') || !WC()->session) {
			return;
		}

		$phone = trim($phone);
		if ($phone === '') {
			return;
		}

		// Already prefetched and fresh.
		$existing = self::get_prefetched_membership_from_session($phone);
		if ($existing instanceof Dobare_Membership) {
			return;
		}

		$session = WC()->session;
		$lock_key = 'dobare_membership_prefetch_lock_' . md5($phone);
		$locked_at = (int) $session->get($lock_key, 0);
		if ($locked_at > 0 && (time() - $locked_at) < 15) {
			return;
		}
		$session->set($lock_key, time());

		$membership = self::fetch_membership_from_api($phone, 'dobare_api_get_member_prefetch');
		if ($membership instanceof Dobare_Membership) {
			self::store_prefetched_membership_in_session($membership);
		}

		// Release lock.
		$session->set($lock_key, 0);
		if (Dobare_Utils::credit_perf_is_enabled() && $reason !== '') {
			Dobare_Utils::credit_perf_record('membership_prefetch_reason', 0.01, array('reason' => $reason));
		}
	}

	/**
	 * Get membership by phone number
	 *
	 * @param string $phone
	 * @return Dobare_Membership|string
	 */
	public static function get_membership_uncached(string $phone)
	{
		if (empty($phone)) {
			return '';
		}

		$phone = trim($phone);
		if ($phone === '') {
			return '';
		}

		if (Dobare_Utils::credit_perf_is_enabled()) {
			Dobare_Utils::credit_perf_record('membership_source', 0.01, array('source' => 'remote_uncached'));
		}

		return self::fetch_membership_from_api($phone, 'dobare_api_get_member_uncached');
	}

	public static function get_membership(string $phone)
	{
		if (empty($phone)) {
			return '';
		}

		$phone = trim($phone);
		if ($phone === '') {
			return '';
		}

		$transient_key = 'dobare_membership_' . md5($phone);
		static $request_cache = array();

		if (array_key_exists($transient_key, $request_cache)) {
			if (Dobare_Utils::credit_perf_is_enabled()) {
				Dobare_Utils::credit_perf_record('membership_cache_request', 0.01);
				Dobare_Utils::credit_perf_record('membership_source', 0.01, array('source' => 'request_cache'));
			}
			return $request_cache[$transient_key];
		}

		$prefetched = self::get_prefetched_membership_from_session($phone);
		if ($prefetched instanceof Dobare_Membership) {
			$request_cache[$transient_key] = $prefetched;
			if (Dobare_Utils::credit_perf_is_enabled() && function_exists('WC') && WC()->session) {
				$cache = WC()->session->get('dobare_membership_prefetch', array());
				$fetched_at = (is_array($cache) && isset($cache[$phone]['fetched_at'])) ? (int) $cache[$phone]['fetched_at'] : 0;
				$age = $fetched_at > 0 ? max(0, time() - $fetched_at) : null;
				Dobare_Utils::credit_perf_record(
					'membership_source',
					0.01,
					array(
						'source' => 'session_prefetch',
						'age_s'  => $age,
					)
				);
			}
			return $prefetched;
		}

		$cached = \get_transient($transient_key);
		if ($cached !== false) {
			$request_cache[$transient_key] = $cached;
			if (Dobare_Utils::credit_perf_is_enabled()) {
				Dobare_Utils::credit_perf_record('membership_cache_transient', 0.05);
				Dobare_Utils::credit_perf_record('membership_source', 0.01, array('source' => 'transient_hit'));
			}
			return $cached;
		}

		if (Dobare_Utils::credit_perf_is_enabled()) {
			Dobare_Utils::credit_perf_record('membership_source', 0.01, array('source' => 'remote_cached'));
		}
		$membership = self::fetch_membership_from_api($phone, 'dobare_api_get_member');
		if (is_wp_error($membership) || (!$membership instanceof Dobare_Membership)) {
			$request_cache[$transient_key] = $membership;
			return $membership;
		}

		// Store into session prefetch cache for the remainder of the checkout session.
		self::store_prefetched_membership_in_session($membership);

		$cache_ttl = (int) apply_filters('dobare_membership_cache_ttl', 60, $phone);
		if ($cache_ttl > 0) {
			\set_transient($transient_key, $membership, $cache_ttl);
		}

		$request_cache[$transient_key] = $membership;

		return $membership;
	}

	/**
	 * Get extended member data by phone number
	 * Uses v1/members/{phone}/ endpoint which provides additional fields
	 *
	 * @param string $phone
	 * @return Dobare_Membership_Extended|string
	 */
	public static function get_member_extended(string $phone)
	{
		if (! empty($phone)) {
			$api_key = Dobare_Utils::get_dob_api_key();
			if (! empty($api_key)) {
				$response = Dobare_REST_Controller::dobare_remote_get('v1/members/' . $phone . '/', array('API-KEY' => $api_key));
				
				if (is_wp_error($response) or $response['status'] != 200) {
					return $response;
				}
				
				$data                 = isset($response['data']['data']) && is_array($response['data']['data']) ? $response['data']['data'] : array();
				$core_fields          = self::extract_member_core_fields($data);
				$birth_date           = !empty($data['birth_date']) ? (string) $data['birth_date'] : null;
				$marriage_date        = !empty($data['marriage_date']) ? (string) $data['marriage_date'] : null;
				$referral_code        = (string) ($data['referral_code'] ?? '');
				$referred_by_code     = (string) ($data['referred_by_code'] ?? '');
				$labels               = (array) ($data['labels'] ?? []);

				return new Dobare_Membership_Extended(
					$core_fields['name'],
					$core_fields['phone'],
					$core_fields['gift_credit'],
					$birth_date,
					$marriage_date,
					$referral_code,
					$referred_by_code,
					$labels
				);
			}
		}

		return '';
	}

	/**
	 * Update member data in Dobare
	 *
	 * @param string $phone Member phone number
	 * @param array $data Data to update (name, birth_date, marriage_date, labels_to_add)
	 * @return array|WP_Error Response array or WP_Error on failure
	 */
	public static function update_member(string $phone, array $data)
	{
		if (empty($phone)) {
			return new \WP_Error('invalid_phone', 'شماره تلفن نامعتبر است', array('status' => 400));
		}

		// Validate date format if provided
		if (isset($data['birth_date']) && !empty($data['birth_date'])) {
			if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $data['birth_date'])) {
				return new \WP_Error('invalid_date_format', 'فرمت تاریخ تولد نامعتبر است. فرمت صحیح: YYYY-MM-DD', array('status' => 400));
			}
		}

		if (isset($data['marriage_date']) && !empty($data['marriage_date'])) {
			if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $data['marriage_date'])) {
				return new \WP_Error('invalid_date_format', 'فرمت تاریخ ازدواج نامعتبر است. فرمت صحیح: YYYY-MM-DD', array('status' => 400));
			}
		}

		$api_key = Dobare_Utils::get_dob_api_key();
		if (empty($api_key)) {
			return new \WP_Error('missing_api_key', 'کلید API یافت نشد', array('status' => 401));
		}

		// Prepare body - only send non-null values
		$body = array();
		if (isset($data['name']) && $data['name'] !== null) {
			$body['name'] = (string) $data['name'];
		}
		if (isset($data['birth_date']) && $data['birth_date'] !== null) {
			$body['birth_date'] = (string) $data['birth_date'];
		}
		if (isset($data['marriage_date']) && $data['marriage_date'] !== null) {
			$body['marriage_date'] = (string) $data['marriage_date'];
		}
		if (isset($data['labels_to_add']) && is_array($data['labels_to_add'])) {
			$body['labels_to_add'] = $data['labels_to_add'];
		}

		$response = Dobare_REST_Controller::dobare_remote_patch('v1/members/' . $phone . '/', $body, array('API-KEY' => $api_key));

		if (is_wp_error($response)) {
			return $response;
		}

		// Clear cached member data
		$transient_name = 'dobare_customer_credit_' . md5($phone);
		\delete_transient($transient_name);
		\delete_transient('dobare_membership_' . md5($phone));

		return $response;
	}

	/**
	 * Enqueue checkout scripts
	 */
	public static function enqueue_checkout_script()
	{
		if (is_checkout() || is_cart()) {
			// Allow enabling credit perf tracing from the page URL for easier debugging.
			if (isset($_GET['dobare_credit_perf']) && function_exists('WC') && WC()->session) {
				Dobare_Utils::credit_perf_enable_for_session();
			}

			$version = DOBARE_VERSION;
			wp_enqueue_style('dobare-checkout-style', plugin_dir_url(DOBARE_MAIN_PLUGIN_FILE) . 'build/assets/dobare-checkout.css', array(), $version);
			if (\Dobare\Utils\Dobare_Utils::is_credit_temporarily_disabled()) {
				wp_add_inline_style(
					'dobare-checkout-style',
					'#dobare-apply-credit-row,#dobare-suppression-row,#dobare-apply-credit-btn{display:none!important;}'
				);
			}
			wp_enqueue_script('dobare-checkout-script', plugin_dir_url(DOBARE_MAIN_PLUGIN_FILE) . 'build/assets/dobare-checkout.js', array('jquery'), $version, true);
			wp_localize_script('dobare-checkout-script', 'dobaAjaxObject', array('ajax_url' => admin_url('admin-ajax.php')));

			// Pass error state to JavaScript for immediate cleanup
			$has_error = WC()->session ? WC()->session->get('dobare_phone_mismatch_error') : '';
			wp_localize_script('dobare-checkout-script', 'dobareErrorState', array('hasError' => !empty($has_error)));
			$suppressed = WC()->session ? (bool) WC()->session->get('dobare_suppress_auto_apply', false) : false;
			$suppressed_since = WC()->session ? (int) WC()->session->get('dobare_suppress_auto_apply_since', 0) : 0;
			wp_localize_script(
				'dobare-checkout-script',
				'dobareSuppressionState',
				array(
					'isSuppressed'   => $suppressed,
					'creditDisabled' => \Dobare\Utils\Dobare_Utils::is_credit_temporarily_disabled(),
					'suppressedSince' => $suppressed_since ? gmdate('c', $suppressed_since) : null,
					'copy' => array(
						'noticeTitle' => esc_html__('اعمال اعتبار باشگاه مشتریان', 'dobare'),
						'noticeMessage' => '',
						'buttonLabel' => esc_html__('اعمال اعتبار باشگاه مشتریان', 'dobare'),
					),
				)
			);

			// Localize REST info for manual apply button
			wp_localize_script(
				'dobare-checkout-script',
				'dobareCheckoutApi',
				array(
					'root' => esc_url_raw(rest_url()),
					'nonce' => wp_create_nonce('wp_rest'),
					'applyEndpoint' => 'dobare/v1/checkout/apply-credit-now',
					'applyLabel' => esc_html__('اعمال اعتبار باشگاه مشتریان', 'dobare')
				)
			);

			// Inline script to handle manual apply button click
(function(){})();

			$inline_js = <<<'JS'
(function($){
  function isValidPhone(value){
    var re=/^(?:\+98|0)?(9\d{9})$/;
    return re.test(String(value||''));
  }

  async function applyDobareCredit(optionalPhone){
    try{
      var $form = $('form.checkout');
      if(!$form.length){
        $(document.body).trigger('update_checkout');
        return;
      }

      // Ensure a hidden flag exists so WooCommerce includes it in update_order_review payload.
      var $flag = $form.find('input[name="dobare_manual_apply_credit"]');
      if(!$flag.length){
        $flag = $('<input type="hidden" name="dobare_manual_apply_credit" value="" />');
        $form.append($flag);
      }
      $flag.val('1');

      // Best-effort: keep the phone field in sync for this request.
      if(optionalPhone){
        var $phoneEl = $('input[id^="billing"][id$="phone"]');
        if($phoneEl.length && String($phoneEl.val()||'') !== String(optionalPhone)){
          $phoneEl.val(String(optionalPhone));
        }
      }

      window.dobareSuppressionState = window.dobareSuppressionState || {};
      window.dobareSuppressionState.isSuppressed = false;
      $(document).trigger('dobare_suppression_cleared');

      await new Promise(function(resolve){
        var done = false;
        function cleanup(){
          if(done){ return; }
          done = true;
          $(document.body).off('updated_checkout.dobare_apply_credit', onUpdated);
          $flag.val('');
          resolve();
        }
        function onUpdated(){
          cleanup();
        }
        $(document.body).on('updated_checkout.dobare_apply_credit', onUpdated);
        // Fallback timeout to avoid getting stuck if the event doesn't fire.
        setTimeout(cleanup, 10000);
        $(document.body).trigger('update_checkout');
      });
    }catch(e){
      try{
        $(document.body).trigger('update_checkout');
      }catch(_e){}
    }
  }
  window.dobareApplyCredit = applyDobareCredit;

  function ensureInlineRow(){
    var $table = $('.woocommerce-checkout-review-order-table');
    if(!$table.length){
      return;
    }
    
    // Get tfoot if it exists, otherwise we'll work with the table directly
    var $tfoot = $table.find('tfoot');
    var $tbody = $table.find('tbody');
    
    // If no tfoot exists, we'll need to create it or work with tbody
    var $targetContainer = $tfoot.length ? $tfoot : $tbody;
    
    if(!$targetContainer.length){
      return;
    }
    
    var hasDobareCoupon = $table.find('tr.cart-discount[class*="coupon-dobare_discount_"]').length>0;
    var $phoneEl = $('input[id^="billing"][id$="phone"]');
    var phone = $phoneEl.length ? $phoneEl.val() : '';
    var phoneOk = isValidPhone(phone);
    var creditDisabled = !!(window.dobareSuppressionState && window.dobareSuppressionState.creditDisabled);

    var $existing = $('#dobare-apply-credit-row');
    if(hasDobareCoupon || !phoneOk || creditDisabled){
      if($existing.length){
        $existing.remove();
      }
      // Remove any legacy/alternate apply button variants, if present.
      $('#dobare-apply-credit-btn').remove();
      // Also hide any server-rendered suppression row during disabled window.
      if(creditDisabled){
        $('#dobare-suppression-row').remove();
        $('#dobare-allow-reapply-button').remove();
        $('#dobare-apply-credit-inline-button').remove();
      }
      return;
    }

    if(!$existing.length){
      var label = String(dobareCheckoutApi.applyLabel || 'اعمال اعتبار باشگاه مشتریان');
      var rowHtml = '<tr id="dobare-apply-credit-row" class="dobare-apply-credit-row"><th>'+label+'</th>'+
                    '<td data-title="'+label+'"><a href="#" id="dobare-apply-credit-inline-button" class="button alt">'+label+'</a></td></tr>';

      var $orderTotal = $targetContainer.find('tr.order-total').first();
      if($orderTotal.length){
        $(rowHtml).insertBefore($orderTotal);
      }else{
        $targetContainer.append(rowHtml);
      }
    }
  }

  function ensureSuppressionRow(){
    var state = window.dobareSuppressionState || {};
    var suppressed = !!state.isSuppressed;
    var creditDisabled = !!state.creditDisabled;
    var $existing = $('#dobare-suppression-row');
    var inlineExists = $('#dobare-apply-credit-row').length > 0;

    if(creditDisabled || !suppressed || inlineExists){
      $existing.remove();
      return;
    }

    var $table = $('.woocommerce-checkout-review-order-table');
    if(!$table.length){
      return;
    }

    var $tfoot = $table.find('tfoot');
    var $tbody = $table.find('tbody');
    var $targetContainer = $tfoot.length ? $tfoot : $tbody;
    if(!$targetContainer.length){
      return;
    }

    var hasDobareCoupon = $table.find('tr.cart-discount[class*="coupon-dobare_discount_"]').length>0;
    var $phoneEl = $('input[id^="billing"][id$="phone"]');
    var phone = $phoneEl.length ? $phoneEl.val() : '';
    var phoneOk = isValidPhone(phone);

    if(hasDobareCoupon || !phoneOk){
      $existing.remove();
      return;
    }

    var copy = state.copy || {};
    var title = String(copy.noticeTitle || copy.buttonLabel || 'اعمال اعتبار باشگاه مشتریان');
    var buttonLabel = String(copy.buttonLabel || title);

    if(!$existing.length){
      var rowHtml = '<tr id="dobare-suppression-row" class="dobare-suppression-row"><th>'+title+'</th>'+
                    '<td data-title="'+title+'"><a href="#" class="button alt" id="dobare-allow-reapply-button">'+buttonLabel+'</a></td></tr>';
      var $orderTotal = $targetContainer.find('tr.order-total').first();
      if($orderTotal.length){
        $(rowHtml).insertBefore($orderTotal);
      }else{
        $targetContainer.append(rowHtml);
      }
    } else {
      $existing.find('th').text(title);
      $existing.find('td').attr('data-title', title);
      $existing.find('#dobare-allow-reapply-button').text(buttonLabel);
    }
  }

$(document).on('click','#dobare-apply-credit-btn',async function(e){
    e.preventDefault();
    if(window.dobareSuppressionState && window.dobareSuppressionState.creditDisabled){
      return;
    }
    var $btn=$(this);
    if($btn.data('loading')) return;
    $btn.data('loading',true);
    var origText=$btn.text();
    $btn.prop('disabled',true).addClass('disabled');
    try{
        var $phoneEl=$('input[id^="billing"][id$="phone"]');
        var phone=$phoneEl.length?$phoneEl.val():'';
        await applyDobareCredit(phone||null);
    }catch(err){}
    finally{
        $btn.prop('disabled',false).removeClass('disabled').data('loading',false).text(origText);
    }
});

  $(document).on('click','#dobare-apply-credit-inline-button', async function(e){
    e.preventDefault();
    if(window.dobareSuppressionState && window.dobareSuppressionState.creditDisabled){
      return;
    }
    var $btn=$(this);
    if($btn.data('loading')) {
      return;
    }

    // INSTANT VISUAL FEEDBACK - disable button and show loading state
    $btn.data('loading',true);
    var origText=$btn.text();
    $btn.prop('disabled',true).addClass('disabled').css('opacity','0.6');

    // INSTANT checkout loading state - trigger BEFORE API call
    $('.woocommerce-checkout-review-order-table').block({
      message: null,
      overlayCSS: {
        background: '#fff',
        opacity: 0.6
      }
    });

    try{
      var $phoneEl=$('input[id^="billing"][id$="phone"]');
      var phone=$phoneEl.length?$phoneEl.val():'';
      await applyDobareCredit(phone||null);
    }catch(err){}
    finally{
      // Restore button state
      $btn.prop('disabled',false).removeClass('disabled').css('opacity','1').text(origText).data('loading',false);
    }
  });

  function syncDobareRows(){
    ensureInlineRow();
    ensureSuppressionRow();
  }

  $(document).on('dobare_suppression_cleared', function(){
    syncDobareRows();
  });

  $(document.body).on('updated_checkout applied_coupon removed_coupon wc_fragments_loaded', syncDobareRows);
  $(function(){ syncDobareRows(); });
})(jQuery);
JS;
			if (function_exists('wp_add_inline_script')) {
				wp_add_inline_script('dobare-checkout-script', $inline_js);
			}
		}
	}

	/**
	 * Render manual Apply Credit button on checkout as a safety net.
	 *
	 * Shows only when a valid phone seems present and no Dobare coupon is applied yet.
	 *
	 * @return void
	 */
	public function render_manual_apply_credit_button(): void
	{
		if (! function_exists('WC') || ! is_checkout()) {
			return;
		}

		if (\Dobare\Utils\Dobare_Utils::is_credit_temporarily_disabled()) {
			return;
		}

		$cart = WC()->cart;
		$session = WC()->session;
		if (! $cart || ! $session) {
			return;
		}

		// If a Dobare coupon is already applied, no need to show the button
		$applied_coupons = $cart->get_applied_coupons();
		foreach ($applied_coupons as $coupon_code) {
			if (\Dobare\Utils\Dobare_Utils::is_dobare_coupon($coupon_code)) {
				return;
			}
		}

		// Heuristic: Only render if we can infer a phone
		$phone = (string) $session->get('dobare_credit_applied_for_phone', '');
		if (empty($phone)) {
			$checkout_data = $session->get('checkout_data');
			if (! empty($checkout_data)) {
				$phone = (string) ($checkout_data['billing_phone'] ?? ($checkout_data['shipping_phone'] ?? ''));
			}
		}
		if (empty($phone)) {
			return;
		}
	}

	/**
	 * Render suppression notice row when customer has explicitly removed credit.
	 *
	 * @return void
	 */
	public function render_suppression_notice_row(): void
	{
		if (!function_exists('WC') || !is_checkout()) {
			return;
		}

		if (\Dobare\Utils\Dobare_Utils::is_credit_temporarily_disabled()) {
			return;
		}

		$cart = WC()->cart;
		$session = WC()->session;

		if (!$cart || !$session) {
			return;
		}

		$auto_suppressed = (bool) $session->get('dobare_suppress_auto_apply', false);
		if (!$auto_suppressed) {
			return;
		}

		$applied_coupons = $cart->get_applied_coupons();
		foreach ($applied_coupons as $coupon_code) {
			if (Dobare_Utils::is_dobare_coupon($coupon_code)) {
				return;
			}
		}

		$phone = (string) $session->get('dobare_credit_applied_for_phone', '');
		if (empty($phone)) {
			return;
		}

		$label = esc_html__('اعمال اعتبار باشگاه مشتریان', 'dobare');

		echo '<tr id="dobare-suppression-row" class="dobare-suppression-row">';
		echo '<th>' . $label . '</th>';
		echo '<td data-title="' . $label . '">';
		echo '<a href="#" class="button alt" id="dobare-allow-reapply-button">' . $label . '</a>';
		echo '</td>';
		echo '</tr>';
	}

	/**
	 * Apply credit on checkout
	 *
	 * @param string $post_data
	 */
	public function apply_credit_on_checkout($post_data)
	{
		if (!WC()->session) {
			return;
		}

		if (\Dobare\Utils\Dobare_Utils::is_credit_temporarily_disabled()) {
			\Dobare\Utils\Dobare_Utils::remove_dobare_coupons();
			WC()->session->set('dobare_minimum_cart_met', false);
			\Dobare\Utils\Dobare_Utils::remove_dobare_success_notices();
			return;
		}

		if (! empty($post_data)) {
			$session_billing_phone = (string) WC()->session->get('dobare_credit_applied_for_phone', '');
			parse_str($post_data, $parsed_data);

			$is_ajax = (defined('DOING_AJAX') && DOING_AJAX) || wp_doing_ajax();
			$current_suppression = WC()->session->get('dobare_suppress_auto_apply', false);
			$submitted_phone_raw = isset($parsed_data['billing_phone']) ? (string) $parsed_data['billing_phone'] : '';
			$normalized_submitted_phone = Dobare_Utils::validate_and_normalize_phone_number($submitted_phone_raw);
			$manual_apply_requested = isset($parsed_data['dobare_manual_apply_credit']) && (string) $parsed_data['dobare_manual_apply_credit'] === '1';

			if ($manual_apply_requested) {
				// One-shot manual apply request: bypass suppression and force apply attempt in this checkout update.
				WC()->session->set('dobare_manual_apply_credit_until', time() + 120);
				Dobare_Utils::set_auto_apply_suppression(false);
				if (!empty($normalized_submitted_phone)) {
					WC()->session->set('dobare_credit_applied_for_phone', $normalized_submitted_phone);
				}
				if (Dobare_Utils::credit_perf_is_enabled()) {
					Dobare_Utils::credit_perf_record('manual_apply_requested', 0.01);
				}
			}

			// Prefetch membership as early as possible so apply does not wait on remote API.
			$prefetch_phone = !empty($normalized_submitted_phone) ? $normalized_submitted_phone : $session_billing_phone;
			if (!empty($prefetch_phone)) {
				self::prefetch_membership_for_session($prefetch_phone, 'checkout_update_order_review');
			}

			// Treat phone as updated only when the normalized value actually changes.
			$phone_changed = (!empty($normalized_submitted_phone) && $normalized_submitted_phone !== $session_billing_phone);

			if ($phone_changed) {
				$billing_phone = $submitted_phone_raw;

				// CRITICAL: Check if there's an active Dobare coupon and validate hash
				$applied_coupons = WC()->cart->get_applied_coupons();
				$dobare_coupon = null;
				$dobare_coupon_code = '';

				foreach ($applied_coupons as $coupon_code) {
					if (Dobare_Utils::is_dobare_coupon($coupon_code)) {
						$dobare_coupon = new \WC_Coupon($coupon_code);
						$dobare_coupon_code = $coupon_code;
						break;
					}
				}

				// If there's a Dobare coupon, validate the hash
				if ($dobare_coupon) {
					$stored_hash = get_post_meta($dobare_coupon->get_id(), '_dobare_phone_hash', true);
					$normalized_new_phone = \Dobare\Utils\Dobare_Utils::validate_and_normalize_phone_number((string) $billing_phone);
					$new_hash = !empty($normalized_new_phone) ? hash('sha256', $normalized_new_phone) : '';

					if (!empty($stored_hash) && $stored_hash !== $new_hash) {
						// Remove the invalid coupon
						WC()->cart->remove_coupon($dobare_coupon_code);

						// CRITICAL: Clear all success notices before showing error
						Dobare_Utils::remove_dobare_success_notices();

						// Store error in session for fragment injection
						$error_message = 'به دلیل تغییر شماره تلفن، اعتبار هدیه پیشین شما حذف شد.' . "\n\n" . 'در صورتی که شماره جدیدتان شامل اعتبار هدیه باشد، می‌توانید با فشردن دکمه «اعمال اعتبار باشگاه مشتریان»، آن را اعمال کنید.';
						WC()->session->set('dobare_phone_mismatch_error', $error_message);
						return;
					}
				}

				$customer = WC()->customer;
				$customer->set_billing_phone($billing_phone);
				$customer->set_shipping_phone($billing_phone);
				$customer->save();
				WC()->session->set('billing_phone', $billing_phone);
				WC()->session->set('shipping_phone', $billing_phone);
				$this->create_membership_from_customer($customer);

				// Store normalized phone for unified revalidation flow
				if (!empty($normalized_submitted_phone)) {
					\WC()->session->set('dobare_credit_applied_for_phone', $normalized_submitted_phone);
					\WC()->session->set('dobare_minimum_cart_met', false);
					\WC()->session->set('dobare_last_phone_for_applied_credit', '');
					// CRITICAL FIX: Clear suppression only when phone truly changed.
					Dobare_Utils::set_auto_apply_suppression(false);
				}
			}
		}
	}

	/**
	 * Upload CSV file with customer data to Dobare API
	 *
	 * @param string $csv_file_path Path to the CSV file
	 * @return array Result array with success status and message
	 */
	public function upload_customers_csv_to_dobare($csv_file_path)
	{
		try {
			// Check if file exists
			if (!file_exists($csv_file_path)) {
				\error_log("Dobare upload: CSV file not found at {$csv_file_path}");
				return [
					'success' => false,
					'message' => 'CSV file not found'
				];
			}

			// Get API key
			$api_key = \Dobare\Utils\Dobare_Utils::get_dob_api_key();

			// Check if API key is configured
			if (empty($api_key)) {
				\error_log("Dobare upload: API key is not configured");
				return [
					'success' => false,
					'message' => 'API key is not configured'
				];
			}

			// Check file size
			$file_size = filesize($csv_file_path);
			if ($file_size === 0) {
				\error_log("Dobare upload: CSV file is empty");
				return [
					'success' => false,
					'message' => 'CSV file is empty'
				];
			}

			if ($file_size > 10 * 1024 * 1024) { // 10MB limit
				\error_log("Dobare upload: CSV file exceeds size limit (10MB)");
				return [
					'success' => false,
					'message' => 'CSV file exceeds size limit (10MB)'
				];
			}

			// Only sample a small portion of the file to verify format (memory efficient)
			$file_handle = fopen($csv_file_path, 'r');
			if ($file_handle) {
				$sample_line = fgets($file_handle, 1024);
				fclose($file_handle);
				\error_log("Dobare upload: Sample CSV header: " . trim($sample_line));
			}

			// Log the upload attempt
			\error_log("Dobare upload: Uploading CSV file of size " . round($file_size / 1024, 2) . "KB to Dobare API");

			// Prepare file for upload - use 'csv_file' as the field name
			$curl_file = new \CURLFile($csv_file_path, 'text/csv', basename($csv_file_path));

			// Use the correct API endpoint for customer sync
			$api_url = 'http://api.dobare.me/v1/integrations/wp/sync-customers/';
			\error_log("Dobare upload: Using API endpoint: {$api_url}");

			// Setup CURL request with memory optimization
			$curl = curl_init();
			curl_setopt_array($curl, [
				CURLOPT_URL => $api_url,
				CURLOPT_RETURNTRANSFER => true,
				CURLOPT_ENCODING => '',
				CURLOPT_MAXREDIRS => 5,               // Reduced from 10
				CURLOPT_TIMEOUT => 300,               // 5 minute timeout for large files
				CURLOPT_CONNECTTIMEOUT => 30,
				CURLOPT_FOLLOWLOCATION => true,
				CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
				CURLOPT_CUSTOMREQUEST => 'POST',
				CURLOPT_POSTFIELDS => [
					'csv_file' => $curl_file,
				],
				CURLOPT_HTTPHEADER => [
					'API-KEY: ' . $api_key,
					'Expect:' // Prevents curl from sending Expect: 100-continue, reducing overhead
				],
				CURLOPT_VERBOSE => false,             // Set to false to reduce log size
				// Memory optimization options
				CURLOPT_BUFFERSIZE => 128 * 1024,     // 128KB buffer size for better memory control
				CURLOPT_SSL_VERIFYPEER => true,
				CURLOPT_SSL_VERIFYHOST => 2
			]);

			// Log the curl execution
			\error_log("Dobare upload: Executing cURL request to Dobare API");

			// Execute request
			$response = curl_exec($curl);
			$curl_error = curl_error($curl);
			$http_code = curl_getinfo($curl, CURLINFO_HTTP_CODE);

			// Log response code
			\error_log("Dobare upload: API response code: {$http_code}");

			// Close curl and free resources
			curl_close($curl);
			unset($curl);
			unset($curl_file);

			// Handle cURL error
			if ($curl_error) {
				\error_log("Dobare upload: cURL error - {$curl_error}");
				return [
					'success' => false,
					'message' => "cURL error: {$curl_error}"
				];
			}

			// Log limited sample of response for debugging
			if (is_string($response) && strlen($response) > 0) {
				\error_log("Dobare upload: API response sample: " . substr($response, 0, 500) . (strlen($response) > 500 ? '...[truncated]' : ''));
			}

			// Parse response
			$response_data = json_decode($response, true);
			unset($response); // Free up memory

			// Check if response is valid JSON
			if (json_last_error() !== JSON_ERROR_NONE) {
				\error_log("Dobare upload: Invalid JSON response");
				return [
					'success' => false,
					'message' => 'Invalid response from Dobare API'
				];
			}

			// Handle API error
			if ($http_code >= 400 || (isset($response_data['error']) && $response_data['error'])) {
				$error_message = isset($response_data['error']) ? $response_data['error'] : "HTTP error code: {$http_code}";
				if (isset($response_data['message'])) {
					$error_message = $response_data['message'];
				}
				\error_log("Dobare upload: API error - {$error_message}");
				return [
					'success' => false,
					'message' => "API error: {$error_message}"
				];
			}

			// Extract remote_id if available
			$remote_id = isset($response_data['remote_id']) ? $response_data['remote_id'] : null;

			// Log successful upload
			\error_log("Dobare upload: Successfully uploaded customers to Dobare API" .
				($remote_id ? " with remote ID: {$remote_id}" : ""));

			return [
				'success' => true,
				'message' => isset($response_data['message']) ? $response_data['message'] : 'Upload completed successfully',
				'remote_id' => $remote_id
			];
		} catch (\Exception $e) {
			\error_log("Dobare upload: Exception - " . $e->getMessage());
			return [
				'success' => false,
				'message' => $e->getMessage()
			];
		}
	}

	/**
	 * Process customer sync directly without scheduling
	 * Uses WooCommerce API instead of direct SQL queries
	 *
	 * @return array
	 */
	public function process_customer_sync_direct()
	{
		try {
			// Set unlimited execution time
			set_time_limit(0);

			\error_log('Dobare sync: Starting direct customer sync process');
			\Dobare\Utils\Dobare_Utils::set_sync_customers_progress('processing', 0, 'Starting customer data collection');

			// First check if there are any orders at all
			$count_args = [
				'limit' => 1,
				'return' => 'ids',
				'status' => 'any',
				'type' => 'shop_order', // Ensure we only get orders, not refunds
			];
			$orders_check = wc_get_orders($count_args);
			$has_orders = !empty($orders_check);

			if (!$has_orders) {
				\error_log('Dobare sync: No orders found in the database');
				\Dobare\Utils\Dobare_Utils::set_sync_customers_progress('error', 0, 'No orders found in the database');
				return ['success' => false, 'message' => 'No orders found in the database'];
			}

			// Create a temp file to store customer data
			$upload_dir = \wp_upload_dir();
			$csv_file = $upload_dir['basedir'] . '/dobare-customers-' . uniqid() . '.csv';
			$csv_handle = fopen($csv_file, 'w');

			if (!$csv_handle) {
				throw new \Exception('Failed to create temporary CSV file');
			}

			// Write CSV header
			fputcsv($csv_handle, ['phone', 'first_name', 'last_name', 'customer_id']);

			// Process orders in batches for memory efficiency
			$batch_size = 100;
			$offset = 0;
			$processed_phones = [];
			$unique_customers = 0;
			$total_processed = 0;

			\Dobare\Utils\Dobare_Utils::set_sync_customers_progress('processing', 5, 'Fetching customer data from orders');

			do {
				// Fetch a batch of orders
				$orders_args = [
					'limit' => $batch_size,
					'offset' => $offset,
					'orderby' => 'date',
					'order' => 'DESC',
					'return' => 'objects',
					'status' => 'any',
					'type' => 'shop_order', // Explicitly request only shop_order, not refunds
				];

				\error_log("Dobare sync: Fetching orders batch at offset {$offset}");
				$orders = wc_get_orders($orders_args);
				$batch_count = count($orders);
				$total_processed += $batch_count;

				if (empty($orders)) {
					break;
				}

				// Process each order in the batch
				foreach ($orders as $index => $order) {
					// Make sure we're dealing with a valid WC_Order and not a refund
					if (!($order instanceof \WC_Order) || $order instanceof \WC_Order_Refund) {
						continue;
					}

					// Verify the order has a get_billing_phone method
					if (!method_exists($order, 'get_billing_phone')) {
						continue;
					}

					// Extract customer data from the order
					$phone = $order->get_billing_phone();

					// Clean and normalize the phone number
					$phone = preg_replace('/[^0-9+]/', '', $phone);
					$normalized_phone = \Dobare\Utils\Dobare_Utils::validate_and_normalize_phone_number($phone);

					// Skip if empty or already processed this phone number
					if (empty($normalized_phone) || isset($processed_phones[$normalized_phone])) {
						continue;
					}

					// Add to processed phones to avoid duplicates
					$processed_phones[$normalized_phone] = true;
					$unique_customers++;

					// Log periodically for debugging
					if ($unique_customers % 100 === 0) {
						\error_log("Dobare sync: Processed {$unique_customers} unique customers");
					}

					// Write this customer to CSV
					fputcsv($csv_handle, [
						$normalized_phone,
						$order->get_billing_first_name() ?? '',
						$order->get_billing_last_name() ?? '',
						$order->get_customer_id() ?? ''
					]);

					// Update progress periodically
					if ($unique_customers % 50 === 0) {
						$progress = 5 + min(70, round(($offset + $index) / ($offset + $batch_count + 1) * 70));
						\Dobare\Utils\Dobare_Utils::set_sync_customers_progress(
							'processing',
							$progress,
							"Processed {$unique_customers} unique customers"
						);
					}
				}

				// Move to next batch
				$offset += $batch_size;

				// Update progress after batch
				$progress = 5 + min(70, round($offset / ($offset + 100) * 70));
				\Dobare\Utils\Dobare_Utils::set_sync_customers_progress(
					'processing',
					$progress,
					"Processed {$total_processed} orders, found {$unique_customers} unique customers"
				);

				// Flush the file after each batch to ensure data is written
				fflush($csv_handle);
			} while ($batch_count >= $batch_size);

			// Close the CSV file
			fclose($csv_handle);

			\error_log("Dobare sync: Completed processing {$unique_customers} unique customers from {$total_processed} total orders");

			if ($unique_customers === 0) {
				// No unique customers extracted
				\unlink($csv_file);
				\error_log('Dobare sync: No unique customers found after processing');
				\Dobare\Utils\Dobare_Utils::set_sync_customers_progress('error', 0, 'No unique customers with valid phone numbers found');
				return [
					'success' => false,
					'message' => 'No unique customers with valid phone numbers found'
				];
			}

			\Dobare\Utils\Dobare_Utils::set_sync_customers_progress(
				'uploading',
				80,
				"Uploading {$unique_customers} customer records to Dobare"
			);

			// Upload the CSV file to Dobare
			$upload_result = $this->upload_customers_csv_to_dobare($csv_file);

			// Delete the temporary file
			if (file_exists($csv_file)) {
				\unlink($csv_file);
			}

			if (isset($upload_result['success']) && $upload_result['success']) {
				\Dobare\Utils\Dobare_Utils::set_sync_customers_progress(
					'completed',
					100,
					"Successfully uploaded {$unique_customers} customers"
				);
				\error_log("Dobare sync: Successfully completed customer sync with {$unique_customers} customers");
				return [
					'success' => true,
					'message' => "Successfully uploaded {$unique_customers} unique customers to Dobare"
				];
			} else {
				$error_message = isset($upload_result['message']) ? $upload_result['message'] : 'Unknown error during upload';
				\Dobare\Utils\Dobare_Utils::set_sync_customers_progress('error', 0, "Upload failed: {$error_message}");
				\error_log("Dobare sync: Upload failed - {$error_message}");
				return [
					'success' => false,
					'message' => "Upload failed: {$error_message}"
				];
			}
		} catch (\Exception $e) {
			\error_log("Dobare sync: Exception during customer sync: " . $e->getMessage());
			\Dobare\Utils\Dobare_Utils::set_sync_customers_progress('error', 0, "Error: " . $e->getMessage());

			// Clean up any temp file if it exists
			if (isset($csv_handle) && is_resource($csv_handle)) {
				fclose($csv_handle);
			}

			if (isset($csv_file) && file_exists($csv_file)) {
				\unlink($csv_file);
			}

			return [
				'success' => false,
				'message' => "Error during sync: " . $e->getMessage()
			];
		}
	}
}
