<?php

declare( strict_types=1 );

namespace Simplyin\Simplyin_W_Plugin\Controller\Ajax\Widget;

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

use Simplyin\Simplyin_W_Plugin\Controller\Ajax\Abstract_Response;
use Simplyin\Simplyin_W_Plugin\Helper\Logger_Helper;
use Simplyin\Simplyin_W_Plugin\Helper\Log_Sanitizer;
use Simplyin\Simplyin_W_Plugin\Lib\Session\Simplyin_Session;
use Simplyin\Simplyin_W_Plugin\Payments\Gateway\Payment_Fields\Payment_Fields;
use Simplyin\Simplyin_W_Plugin\Service\Geolocation\Closest_Parcel_Lockers_Request_Builder;
use Simplyin\Simplyin_W_Plugin\Shipping_Mapping\Mapping;


class Widget_Controller {

	private const ADD_PLUGIN_INFO = true;

	public $warnings = [];

	private $allowed_types = [
		'autocomplete',
		'confirmation',
		'delivery',
		'sms',
	];

	private Simplyin_Session $simplyin_session;

	private function print_r( $value, bool $return = true ): string {
		return Logger_Helper::print_r_truncate( $value );
	}

	public function __construct() {
		$this->simplyin_session = simplyin()->get_simplyin_session();
	}

	public function register_ajax_hooks() {

		add_action(
			'wp_ajax_simplyin_session_status',
			[
				$this,
				'session_status_ajax_action',
			]
		);
		add_action(
			'wp_ajax_nopriv_simplyin_session_status',
			[
				$this,
				'session_status_ajax_action',
			]
		);

		add_action(
			'wp_ajax_simplyin_submit_email',
			[
				$this,
				'submit_email_ajax_action',
			]
		);
		add_action(
			'wp_ajax_nopriv_simplyin_submit_email',
			[
				$this,
				'submit_email_ajax_action',
			]
		);

		add_action(
			'wp_ajax_simplyin_get_new_account_form',
			[
				$this,
				'get_new_account_form_ajax_action',
			]
		);
		add_action(
			'wp_ajax_nopriv_simplyin_get_new_account_form',
			[
				$this,
				'get_new_account_form_ajax_action',
			]
		);

		add_action(
			'wp_ajax_simplyin_save_create_account_flag',
			[
				$this,
				'save_create_account_flag_ajax_action',
			]
		);
		add_action(
			'wp_ajax_nopriv_simplyin_save_create_account_flag',
			[
				$this,
				'save_create_account_flag_ajax_action',
			]
		);

		add_action(
			'wp_ajax_simplyin_check_push_notification',
			[
				$this,
				'check_push_notification_ajax_action',
			]
		);
		add_action(
			'wp_ajax_nopriv_simplyin_check_push_notification',
			[
				$this,
				'check_push_notification_ajax_action',
			]
		);

		add_action(
			'wp_ajax_simplyin_send_shop_login_email_otp',
			[
				$this,
				'send_shop_login_email_otp_ajax_action',
			]
		);
		add_action(
			'wp_ajax_nopriv_simplyin_send_shop_login_email_otp',
			[
				$this,
				'send_shop_login_email_otp_ajax_action',
			]
		);

		add_action(
			'wp_ajax_simplyin_verify_shop_login_email_otp',
			[
				$this,
				'verify_shop_login_email_otp_ajax_action',
			]
		);
		add_action(
			'wp_ajax_nopriv_simplyin_verify_shop_login_email_otp',
			[
				$this,
				'verify_shop_login_email_otp_ajax_action',
			]
		);

		add_action(
			'wp_ajax_simplyin_send_email_code',
			[
				$this,
				'send_email_code_ajax_action',
			]
		);
		add_action(
			'wp_ajax_nopriv_simplyin_send_email_code',
			[
				$this,
				'send_email_code_ajax_action',
			]
		);

		add_action(
			'wp_ajax_simplyin_check_code',
			[
				$this,
				'check_code_ajax_action',
			]
		);
		add_action(
			'wp_ajax_nopriv_simplyin_check_code',
			[
				$this,
				'check_code_ajax_action',
			]
		);

		add_action(
			'wp_ajax_simplyin_send_sms_code',
			[
				$this,
				'send_sms_code_ajax_action',
			]
		);
		add_action(
			'wp_ajax_nopriv_simplyin_send_sms_code',
			[
				$this,
				'send_sms_code_ajax_action',
			]
		);

		add_action(
			'wp_ajax_simplyin_select_address',
			[
				$this,
				'select_address_ajax_action',
			]
		);
		add_action(
			'wp_ajax_nopriv_simplyin_select_address',
			[
				$this,
				'select_address_ajax_action',
			]
		);

		add_action(
			'wp_ajax_simplyin_edit_address',
			[
				$this,
				'edit_address_ajax_action',
			]
		);
		add_action(
			'wp_ajax_nopriv_simplyin_edit_address',
			[
				$this,
				'edit_address_ajax_action',
			]
		);

		add_action(
			'wp_ajax_simplyin_delete_address',
			[
				$this,
				'delete_address_ajax_action',
			]
		);
		add_action(
			'wp_ajax_nopriv_simplyin_delete_address',
			[
				$this,
				'delete_address_ajax_action',
			]
		);

		add_action(
			'wp_ajax_simplyin_get_form',
			[ $this, 'get_form_ajax_action' ]
		);
		add_action(
			'wp_ajax_nopriv_simplyin_get_form',
			[ $this, 'get_form_ajax_action' ]
		);

		add_action(
			'wp_ajax_simplyin_user_data_commerce',
			[
				$this,
				'user_data_commerce_ajax_action',
			]
		);
		add_action(
			'wp_ajax_nopriv_simplyin_user_data_commerce',
			[
				$this,
				'user_data_commerce_ajax_action',
			]
		);

		add_action(
			'wp_ajax_simplyin_search_address',
			[
				$this,
				'search_address_ajax_action',
			]
		);
		add_action(
			'wp_ajax_nopriv_simplyin_search_address',
			[
				$this,
				'search_address_ajax_action',
			]
		);

		add_action(
			'wp_ajax_simplyin_get_closest_parcel_lockers',
			[
				$this,
				'get_closest_parcel_lockers_ajax_action',
			]
		);
		add_action(
			'wp_ajax_nopriv_simplyin_get_closest_parcel_lockers',
			[
				$this,
				'get_closest_parcel_lockers_ajax_action',
			]
		);

		add_action(
			'wp_ajax_simplyin_add_address',
			[
				$this,
				'add_address_ajax_action',
			]
		);
		add_action(
			'wp_ajax_nopriv_simplyin_add_address',
			[
				$this,
				'add_address_ajax_action',
			]
		);

		add_action(
			'wp_ajax_simplyin_submit_address',
			[
				$this,
				'submit_address_ajax_action',
			]
		);
		add_action(
			'wp_ajax_nopriv_simplyin_submit_address',
			[
				$this,
				'submit_address_ajax_action',
			]
		);

		add_action(
			'wp_ajax_simplyin_logout',
			[
				$this,
				'logout_ajax_action',
			]
		);

		add_action(
			'wp_ajax_nopriv_simplyin_logout',
			[
				$this,
				'logout_ajax_action',
			]
		);
	}


	public function get_form_ajax_action(): void {
		$type = '';
		if ( isset( $_POST['type'] ) ) {
			if ( check_ajax_referer(
				'simplyin_ajax',
				'ajax_token',
				false
			) ) {

				if ( isset( $_POST['type'] ) ) {
					$type = sanitize_text_field( wp_unslash( $_POST['type'] ) );
				} else {
					$type = '';
				}
			} else {
				$this->output_response_unauthorized();
			}
		}

		$resp = new Get_Form_Response();

		if ( ! in_array( $type, $this->allowed_types, true ) ) {
			$resp->set_is_success( false );
			$resp->set_error( 'INVALID_FORM_TYPE' );
			$this->output_response( $resp );

			return;
		}

		$html = $this->render_ajax_form( $type );

		$resp->set_is_success( true );
		$resp->set_html( $html );
		$this->output_response( $resp );
	}

	private function setNotificationId(
		string $email,
		$api_response = null
	): void {
		$notification_token_id = '';
		if ( is_object( $api_response ) && isset( $api_response->notificationTokenId ) ) {
			$notification_token_id = (string) $api_response->notificationTokenId;
		} elseif ( is_string( $api_response ) && '' !== $api_response ) {
			$notification_token_id = $api_response;
		}

		if ( '' !== $notification_token_id ) {
			$this->simplyin_session->set(
				'simplyin_notification_token_id',
				$notification_token_id
			);
		}

		$this->simplyin_session->set( 'simplyin_pending_email', $email );
		$this->simplyin_session->set( 'simplyin_push_confirmed', false );
	}

	private function output_response( Abstract_Response $response ) {
		$response_arr = $response->to_array();

		simplyin()->get_simplyin_logger()->log_if_wpdebug_on(
			sprintf(
				'[Widget_Controller] [output_response] %s',
				$this->print_r(
					[
						'response_arr' => $response_arr,
					],
					true
				),
			),
			'widget_ajax'
		);

		wp_send_json( $response_arr );
	}

	/**
	 * @return never wp_send_json_error() terminates the request (wp_die()) - execution never
	 *   continues past this call, so callers can safely rely on it as their error exit path.
	 */
	private function output_response_bad_request() {
		wp_send_json_error( [ 'code' => 'BAD_REQUEST' ], 400 );
	}

	/**
	 * @return never wp_send_json_error() terminates the request (wp_die()) - execution never
	 *   continues past this call, so callers can safely rely on it as their error exit path.
	 */
	private function output_response_unauthorized() {
		wp_send_json_error( [ 'code' => 'UNAUTHORIZED' ], 401 );
	}

	public function session_status_ajax_action(): void {
		if ( ! check_ajax_referer( 'simplyin_ajax', 'ajax_token', false ) ) {
			$this->output_response_unauthorized();
			return;
		}

		$logger = simplyin()->get_simplyin_logger();

		$session_email         = (string) $this->simplyin_session->get(
			'simplyin_email',
			''
		);
		$auth_token            = (string) $this->simplyin_session->get(
			'simplyin_auth_token',
			''
		);
		$notification_token_id = (string) $this->simplyin_session->get(
			'simplyin_notification_token_id',
			''
		);
		$is_push_confirmed     = (bool) $this->simplyin_session->get(
			'simplyin_push_confirmed',
			false
		);

		$logger->log_if_wpdebug_on(
			'[session_status] SESSION ' . $this->print_r(
				Log_Sanitizer::mask( compact(
					'session_email',
					'auth_token',
					'notification_token_id',
					'is_push_confirmed'
				) ),
				true
			),
			'widget_ajax'
		);

		$is_local_session_valid = ( '' !== $auth_token && ( '' === $notification_token_id || $is_push_confirmed ) );
		if ( ! $is_local_session_valid ) {
			$response = new Check_Push_Notification_Response();
			$response->set_is_success( false );
			$response->set_error( '' !== $notification_token_id && ! $is_push_confirmed ? 'PUSH_NOT_CONFIRMED' : 'NO_ACTIVE_SESSION' );

			if ( 'NO_ACTIVE_SESSION' === $response->get_error() ) {
				$this->reset_simplyin_session();
			}

			$this->output_response( $response );

			return;
		}

		try {
			$user_data_payload = $this->user_data_commerce_ajax_action();
		} catch ( \Throwable $e ) {
			$response = new Check_Push_Notification_Response();
			$response->set_is_success( false );
			$response->set_error( 'API_ERROR' );
			$this->output_response( $response );

			return;
		}

		if ( ! empty( $user_data_payload['code'] ) ) {
			$response = new Check_Push_Notification_Response();
			$response->set_is_success( false );
			$response->set_error( $user_data_payload['code'] );
			$this->output_response( $response );

			return;
		}

		if ( ! isset( $user_data_payload['data'] ) && isset( $user_data_payload['userData'] ) ) {
			$user_data_payload['data'] = $user_data_payload['userData'];
		}

		$user_data = $user_data_payload['data'] ?? [];
		$api_email = isset( $user_data['email'] ) ? (string) $user_data['email'] : '';

		if ( '' !== $api_email && '' !== $session_email && 0 !== strcasecmp(
			$api_email,
			$session_email
		) ) {
			$response = new Check_Push_Notification_Response();
			$response->set_is_success( false );
			$response->set_error( 'EMAIL_MISMATCH' );
			$this->output_response( $response );

			return;
		}

		$login_summary = [
			'is_guest'  => true,
			'firstname' => $user_data['name'] ?? '',
			'lastname'  => $user_data['surname'] ?? '',
			'email'     => $session_email,
		];

		$checkout_sync = $this->sync_checkout_delivery_from_commerce_payload(
			$user_data_payload
		);

		$delivery_form_html = $this->render_delivery_form( [ 'data' => $user_data ] );

		$response = new Check_Push_Notification_Response();
		$response->set_is_success( true );
		$response->set_login_result( $login_summary );
		$response->set_html( $delivery_form_html );
		$this->apply_invoice_and_phone_to_login_response( $response, $user_data, $checkout_sync );

		$logger->log_if_wpdebug_on(
			'[session_status] OUTPUT ' . $this->print_r(
				$response->to_array(),
				true
			),
			'widget_ajax'
		);

		$this->output_response( $response );
	}

	public function submit_email_ajax_action(): void {

		if ( check_ajax_referer(
			'simplyin_ajax',
			'ajax_token',
			false
		) ) {

			if ( isset( $_POST['email'] ) ) {
				$email = sanitize_email( wp_unslash( $_POST['email'] ) );
			} else {
				$this->output_response_bad_request();
			}

			if ( isset( $_POST['logout'] ) ) {
				$logout = sanitize_text_field( wp_unslash( $_POST['logout'] ) );
				$logout = in_array(
					$logout,
					[ '1', 'true', 'yes', 'on' ],
					true
				);
			} else {
				$this->output_response_bad_request();
			}
		} else {
			$this->output_response_unauthorized();
		}

		$context = isset( $_POST['context'] ) ? sanitize_key( wp_unslash( $_POST['context'] ) ) : '';
		$this->simplyin_session->set( 'simplyin_context', $context );

		$session        = $this->simplyin_session;
		$pending_token  = $session->get( 'simplyin_notification_token_id', '' );
		$push_confirmed = $session->get( 'simplyin_push_confirmed', false );
		$stored_email   = $session->get( 'simplyin_email', '' );
		$pending_email  = (string) $session->get(
			'simplyin_pending_email',
			''
		);
		$current_email  = $email;

		/*
		 * If the widget explicitly requests a logout OR the user typed a different e-mail address
		 * than the one stored in the session, we must invalidate any previously saved tokens.
		 * Otherwise the next order would still be created for the previous account. This must run
		 * BEFORE the My Account no-WC-account guard below (and before the pending-token popup
		 * check) so a stale token/pending-email from an earlier, different e-mail never survives
		 * an unrelated e-mail submitted afterwards, on the My Account page or elsewhere.
		 */
		if ( $logout || ( $stored_email && 0 !== strcasecmp(
			$stored_email,
			$current_email
		) ) || ( $pending_email && 0 !== strcasecmp(
			$pending_email,
			$current_email
		) ) ) {
			$this->invalidate_simplyin_session_tokens();
			$pending_token = '';
			$pending_email = '';
		}

		// My Account login page: this action can only ever log the shopper into an existing WC
		// account (see login_customer_by_email()), so it must never contact the Simply.IN backend —
		// and never trigger a phone push — for an e-mail with no local WC account. Checkout keeps
		// the unconditional behaviour: recognizing a Simply.IN customer who is new to this shop is
		// the intended 1-click flow there. Mirrors the same local-first guard already used by
		// send_shop_login_email_otp_ajax_action(): answered locally, no backend call. The response
		// reuses the same shape as the real NOT_FOUND branch below (not a new one) so a caller
		// cannot use it as a WC-account-existence oracle.
		if ( 'myaccount' === $context && ! get_user_by( 'email', $email ) ) {
			simplyin()->get_simplyin_logger()->log_if_wpdebug_on(
				'[Widget_Controller] [submit_email_ajax_action] myaccount context, no WC account for e-mail — skipping Simply.IN backend call'
			);
			$response = new Submit_Email_Response();
			$response->set_is_success( false );
			$response->set_error( 'NOT_FOUND' );
			$this->output_response( $response );

			return;
		}

		if ( $pending_token && ! $push_confirmed && $pending_email === $current_email ) {
			$response = new Submit_Email_Response();
			$response->set_is_success( true );
			$response->set_show_popup( true );
			$response->set_user_used_push_notifications( true );
			$response->set_html(
				$this->get_popup_confirmation_html(
					'',
					'',
					'',
					true
				)
			);
			$this->output_response( $response );

			return;
		}

		$api   = simplyin()->get_api();
		$login = false;

		$merchantToken         = get_option( 'simplyin_api_key' );
		$simplyin_api_response = $api->sendRequest(
			'checkout/submitEmail',
			'POST',
			$this->update_array_with_plugin_info(
				[
					'email'  => $email,
					'apiKey' => $merchantToken,
				]
			)
		);

		simplyin()->get_simplyin_logger()->log_if_wpdebug_on(
			sprintf(
				'[Widget_Controller] [submit_email_ajax_action] %s',
				$this->print_r(
					[
						'api response' => $simplyin_api_response,
					],
					true
				),
			),
			'widget_ajax'
		);

		$ajax_response = new Submit_Email_Response();

		if ( is_object( $simplyin_api_response ) && isset( $simplyin_api_response->code ) ) {
			if ( 'NOT_FOUND' === $simplyin_api_response->code ) {
				$ajax_response->set_is_success( false );
				$ajax_response->set_error( $simplyin_api_response->code );
				$this->output_response( $ajax_response );

				return;
			}

			if ( 'UNAUTHORIZED' === $simplyin_api_response->code ) {
				$ajax_response->set_is_success( false );
				$ajax_response->set_error( 'UNAUTHORIZED' );
				$ajax_response->set_show_popup( false );
				simplyin()->handle_api_unauthorized();
				$this->output_response( $ajax_response );

				return;
			}
		} else {
			$ajax_response->set_is_success( false );
			$ajax_response->set_error( 'API_ERROR' );
			$this->output_response( $ajax_response );

			return;
		}

		$ajax_response->set_is_success( true );
		$ajax_response->set_show_popup( true );

		if ( ! empty( $simplyin_api_response->notificationTokenId ) ) {
			$this->simplyin_session->set(
				'simplyin_notification_token_id',
				$simplyin_api_response->notificationTokenId
			);
		}

		$session->set( 'simplyin_pending_email', $current_email );

		simplyin()->get_simplyin_logger()->log_if_wpdebug_on(
			sprintf(
				'[Widget_Controller] [submit_email_ajax_action] %s',
				$this->print_r(
					[
						'simplyin_session_email' => $this->simplyin_session->get( 'simplyin_email' ),
					],
					true
				),
			),
			'widget_ajax'
		);
		$user_used_push_notifications = isset( $simplyin_api_response->userUsedPushNotifications ) && $simplyin_api_response->userUsedPushNotifications;
		$ajax_response->set_user_used_push_notifications( $user_used_push_notifications );

		$code    = $simplyin_api_response->code;
		$data    = $simplyin_api_response->data;
		$message = $simplyin_api_response->message;
		$html    = $this->get_popup_confirmation_html(
			$code,
			$data ?: '',
			$message,
			$user_used_push_notifications,
			$simplyin_api_response->codeSentStatusMessage ?? ''
		);

		$ajax_response->set_html( $html );

		$this->output_response( $ajax_response );
	}

	public function send_sms_code_ajax_action(): void {

		if ( check_ajax_referer(
			'simplyin_ajax',
			'ajax_token',
			false
		) ) {

			if ( isset( $_POST['email'] ) ) {
				$email = sanitize_email( wp_unslash( $_POST['email'] ) );
			} else {
				$this->output_response_bad_request();
			}
		} else {
			$this->output_response_unauthorized();
		}

		simplyin()->get_simplyin_logger()->log_if_wpdebug_on(
			'[send_sms_code] INPUT: ' . $this->print_r(
				[ 'email' => $email ],
				true
			),
			'widget_ajax'
		);

		$api           = simplyin()->get_api();
		$merchantToken = get_option( 'simplyin_api_key' );
		$api_response  = $api->sendRequest(
			'checkout/submitEmail',
			'POST',
			$this->update_array_with_plugin_info(
				[
					'email'    => $email,
					'apiKey'   => $merchantToken,
					'forceSms' => true,
					'isResend' => true,
				]
			)
		);

		simplyin()->get_simplyin_logger()->log_if_wpdebug_on(
			'[send_sms_code] API RESPONSE: ' . $this->print_r(
				$api_response,
				true
			),
			'widget_ajax'
		);

		$response = new Send_SMS_Code_Response();

		if ( ! is_object( $api_response ) || 'NOT_FOUND' === ( $api_response->code ?? '' ) ) {
			$response->set_is_success( false );
			$response->set_error( $api_response->code ?? 'NOT_FOUND' );
			$this->output_response( $response );

			return;
		}

		if ( 'UNAUTHORIZED' === ( $api_response->code ?? '' ) ) {
			$response->set_is_success( false );
			$response->set_error( 'UNAUTHORIZED' );
			$response->set_show_popup( false );
			simplyin()->handle_api_unauthorized();
			$this->output_response( $response );

			return;
		}

		$this->simplyin_session->set(
			'simplyin_notification_token_id',
			$api_response->notificationTokenId ?? ''
		);
		$this->simplyin_session->set( 'simplyin_pending_email', $email );

		$response->set_is_success( true );
		$response->set_show_popup( true );
		$response->set_notification_token_id( $api_response->notificationTokenId ?? '' );
		$response->set_user_used_push_notifications( $api_response->userUsedPushNotifications ?? false );
		$response->set_html(
			$this->get_popup_confirmation_html(
				$api_response->code,
				$api_response->data ?? '',
				$api_response->message ?? '',
				$api_response->userUsedPushNotifications ?? false,
				$api_response->codeSentStatusMessage ?? ''
			)
		);

		$this->output_response( $response );
	}

	public function send_email_code_ajax_action(): void {

		if ( check_ajax_referer(
			'simplyin_ajax',
			'ajax_token',
			false
		) ) {

			if ( isset( $_POST['email'] ) ) {
				$email = sanitize_email( wp_unslash( $_POST['email'] ) );
			} else {
				$this->output_response_bad_request();
			}
		} else {
			$this->output_response_unauthorized();
		}

		simplyin()->get_simplyin_logger()->log_if_wpdebug_on(
			'[Widget_Controller] [send_email_code_ajax_action] INPUT: ' . $this->print_r(
				[ 'email' => $email ],
				true
			),
			'widget_ajax'
		);

		$api           = simplyin()->get_api();
		$merchantToken = get_option( 'simplyin_api_key' );
		$api_response  = $api->sendRequest(
			'checkout/resend-checkout-code-via-email',
			'POST',
			[
				'email'  => $email,
				'apiKey' => $merchantToken,
			]
		);

		simplyin()->get_simplyin_logger()->log_if_wpdebug_on(
			'[Widget_Controller] [send_email_code_ajax_action] API RESPONSE: ' . $this->print_r(
				$api_response,
				true
			),
			'widget_ajax'
		);

		$response = new Send_Email_Code_Response();

		if ( ! is_object( $api_response ) || 'NOT_FOUND' === ( $api_response->code ?? '' ) ) {
			$response->set_is_success( false );
			$response->set_error( $api_response->code ?? 'NOT_FOUND' );
			$this->output_response( $response );

			return;
		}

		if ( 'UNAUTHORIZED' === ( $api_response->code ?? '' ) ) {
			$response->set_is_success( false );
			$response->set_error( 'UNAUTHORIZED' );
			$response->set_show_popup( false );
			simplyin()->handle_api_unauthorized();
			$this->output_response( $response );

			return;
		}

		$session_token   = (string) $this->simplyin_session->get(
			'simplyin_notification_token_id',
			''
		);
		$effective_token = $api_response->notificationTokenId ?? $session_token;
		$this->setNotificationId( $email, $effective_token );

		$response->set_is_success( true );
		$response->set_show_popup( true );
		$response->set_notification_token_id( $effective_token );
		$response->set_user_used_push_notifications( $api_response->userUsedPushNotifications ?? false );
		$response->set_html(
			$this->get_popup_confirmation_html(
				$api_response->code ?? '',
				$api_response->data ?? '',
				$api_response->message ?? '',
				$api_response->userUsedPushNotifications ?? false,
				$api_response->codeSentStatusMessage ?? ''
			)
		);

		$this->output_response( $response );
	}

	private function get_html( string $name, array $args = [] ): string {
		ob_start();

		simplyin()->locate_template(
			$name,
			[ 'args' => $args ],
			'src/Views/Widget'
		);

		$output = ob_get_contents();
		ob_end_clean();

		return (string) $output;
	}

	public function get_popup_confirmation_html(
		string $code,
		string $data,
		string $message,
		bool $user_used_push_notifications,
		string $code_sent_status_message = ''
	) {

		if ( $user_used_push_notifications ) {
			$context = (string) $this->simplyin_session->get( 'simplyin_context', '' );
			return $this->get_html( 'confirmation_form.php', [ 'context' => $context ] );
		} else {
			return $this->get_html(
				'code_form.php',
				[
					'code'                  => $code,
					'data'                  => $data,
					'message'               => $message,
					'codeSentStatusMessage' => $code_sent_status_message,
				]
			);
		}
	}

	public function check_code_ajax_action(): void {

		$logger = simplyin()->get_simplyin_logger();

		if ( check_ajax_referer(
			'simplyin_ajax',
			'ajax_token',
			false
		) ) {

			if ( isset( $_POST['email'] ) ) {
				$email = sanitize_email( wp_unslash( $_POST['email'] ) );
			} else {
				$this->output_response_bad_request();
			}

			if ( isset( $_POST['code'] ) ) {
				$code = sanitize_text_field( wp_unslash( $_POST['code'] ) );
			} else {
				$this->output_response_bad_request();
			}
		} else {
			$this->output_response_unauthorized();
		}

		$notification_token_id = (string) $this->simplyin_session->get(
			'simplyin_notification_token_id',
			''
		);

		$logger->log_if_wpdebug_on(
			'[check_code] INPUT: ' . $this->print_r(
				compact( 'code', 'email' ),
				true
			),
			'widget_ajax'
		);

		$api           = simplyin()->get_api();
		$merchantToken = get_option( 'simplyin_api_key' );
		$api_response  = $api->sendRequest(
			'checkout/submitCheckoutCodeCommerce',
			'POST',
			[
				'email'               => $email,
				'code'                => $code,
				'notificationTokenId' => $notification_token_id,
				'apiKey'              => $merchantToken,
			]
		);

		$logger->log_if_wpdebug_on(
			'[check_code] API RESPONSE: ' . $this->print_r(
				$api_response,
				true
			),
			'widget_ajax'
		);

		$ajax_response = new Check_Code_Response();

		if ( ! is_object( $api_response ) || ( empty( $api_response->isCodeValid ) && empty( $api_response->authToken ) ) ) {
			$ajax_response->set_is_success( false );
			$ajax_response->set_error( $api_response->errorCode ?? 'INVALID_CODE' );
			$this->output_response( $ajax_response );

			return;
		}

		if ( 'UNAUTHORIZED' === ( $api_response->code ?? '' ) ) {
			$ajax_response->set_is_success( false );
			$ajax_response->set_error( 'UNAUTHORIZED' );
			simplyin()->handle_api_unauthorized();
			$this->output_response( $ajax_response );

			return;
		}

		$previous_token = (string) $this->simplyin_session->get( 'simplyin_auth_token', '' );
		$session_id     = (string) $this->simplyin_session->get( 'id', '' );
		Payment_Fields::clear_methods_cache( $previous_token, $session_id );

		$this->simplyin_session->set(
			'simplyin_auth_token',
			$api_response->authToken ?? ''
		);
		$this->simplyin_session->set(
			'simplyin_refresh_token',
			$api_response->refreshToken ?? ''
		);
		Payment_Fields::clear_methods_cache( (string) ( $api_response->authToken ?? '' ), $session_id );
		$this->simplyin_session->set( 'simplyin_email', $email );
		$this->simplyin_session->set( 'simplyin_pending_email', '' );
		$this->simplyin_session->set( 'simplyin_push_confirmed', true );
		$this->simplyin_session->set( 'simplyin_notification_token_id', '' );

		Payment_Fields::clear_methods_cache( (string) ( $api_response->authToken ?? '' ), $session_id );

		$this->simplyin_session->set( 'simplyin_user_data_commerce', null );
		$this->simplyin_session->set(
			'simplyin_user_data_commerce_timestamp',
			null
		);

		$login_result = $this->login_customer_by_email( $email, $api_response );

		if ( isset( $login_result['error'] ) ) {
			$ajax_response->set_is_success( false );
			$ajax_response->set_error( $login_result['error'] );
			$this->output_response( $ajax_response );
			return;
		}

		$payload = json_decode(
			json_encode( $api_response->data ?? [] ),
			true
		);
		if ( ! is_array( $payload ) ) {
			$payload = [];
		}

		$commerce_for_sync = [ 'data' => $payload ];
		$checkout_sync = $this->sync_checkout_delivery_from_commerce_payload(
			$commerce_for_sync
		);

		$html = $this->render_delivery_form( [ 'data' => $payload ] );

		$ajax_response->set_is_success( true );
		$ajax_response->set_login_result( $login_result );
		$ajax_response->set_html( $html );
		$this->apply_invoice_and_phone_to_login_response( $ajax_response, $payload, $checkout_sync );

		$this->output_response( $ajax_response );
	}

	public function check_push_notification_ajax_action(): void {

		$logger = simplyin()->get_simplyin_logger();

		$session_email_confirmed = (string) $this->simplyin_session->get(
			'simplyin_email',
			''
		);
		$session_email_pending   = (string) $this->simplyin_session->get(
			'simplyin_pending_email',
			''
		);
		$email_from_session      = $session_email_pending ?: $session_email_confirmed;

		$notification_token_id = (string) $this->simplyin_session->get(
			'simplyin_notification_token_id',
			''
		);

		$api            = simplyin()->get_api();
		$merchant_token = get_option( 'simplyin_api_key' );

		if ( check_ajax_referer(
			'simplyin_ajax',
			'ajax_token',
			false
		) ) {

		} else {
			$this->output_response_unauthorized();
		}

		$logger->log_if_wpdebug_on(
			sprintf(
				'[Widget_Controller] [check_push_notification_ajax_action] session data %s',
				$this->print_r(
					Log_Sanitizer::mask( [
						'notification_token_id' => $notification_token_id,
						'email_confirmed'       => $session_email_confirmed,
						'email_pending'         => $session_email_pending,
					] ),
					true
				)
			),
			'widget_ajax'
		);

		$email = $email_from_session;

		if ( check_ajax_referer(
			'simplyin_ajax',
			'ajax_token',
			false
		) ) {

			if ( isset( $_POST['email'] ) ) {
				$email = sanitize_email( wp_unslash( $_POST['email'] ) );
			}
		} else {
			$this->output_response_unauthorized();
		}

		$ajax_response = new Check_Push_Notification_Response();

		if ( empty( $email ) || empty( $notification_token_id ) ) {
			$ajax_response->set_is_success( false );
			$ajax_response->set_error( 'MISSING_DATA' );
			$this->output_response( $ajax_response );

			return;
		}

		if ( check_ajax_referer(
			'simplyin_ajax',
			'ajax_token',
			false
		) ) {
			$logger->log_if_wpdebug_on(
				sprintf(
					'[Widget_Controller] [check_push_notification_ajax_action] INPUT %s',
					$this->print_r(
						Log_Sanitizer::mask( [
							'email'               => $email,
							'notificationTokenId' => $notification_token_id,
							'user_logged'         => is_user_logged_in() ? get_current_user_id() : 0,
						] ),
						true
					)
				),
				'widget_ajax'
			);
		} else {
			$this->output_response_unauthorized();
		}

		$api_response = $api->sendRequest(
			'checkout/checkIfSubmitEmailPushNotificationWasConfirmedCommerce',
			'POST',
			[
				'email'               => $email,
				'notificationTokenId' => $notification_token_id,
				'apiKey'              => $merchant_token,
			]
		);

		$is_ok = is_object( $api_response ) && ! empty( $api_response->ok );
		if ( ! $is_ok ) {

			$ajax_response->set_is_success( false );
			$ajax_response->set_error( $api_response->code ?? 'BAD_REQUEST' );

			$this->output_response( $ajax_response );

			return;
		}

		$this->simplyin_session->set( 'simplyin_push_confirmed', true );
		$this->simplyin_session->set( 'simplyin_email', $email );
		$this->simplyin_session->set( 'simplyin_pending_email', '' );
		$this->simplyin_session->set(
			'simplyin_auth_token',
			$api_response->authToken ?? ''
		);

		$this->simplyin_session->set(
			'simplyin_refresh_token',
			$api_response->refreshToken ?? ''
		);

		$this->simplyin_session->set( 'simplyin_user_data_commerce', null );
		$this->simplyin_session->set(
			'simplyin_user_data_commerce_timestamp',
			null
		);

		$login_result = $this->login_customer_by_email( $email, $api_response );

		if ( isset( $login_result['error'] ) ) {
			$ajax_response->set_is_success( false );
			$ajax_response->set_error( $login_result['error'] );
			$this->output_response( $ajax_response );
			return;
		}

		$user_data_arr = json_decode(
			json_encode( $api_response->userData ),
			true
		);
		if ( ! is_array( $user_data_arr ) ) {
			$user_data_arr = [];
		}

		$commerce_for_sync = [ 'data' => $user_data_arr ];
		$checkout_sync = $this->sync_checkout_delivery_from_commerce_payload(
			$commerce_for_sync
		);

		$html = $this->render_delivery_form( [ 'data' => $user_data_arr ] );

		$ajax_response->set_is_success( true );
		$ajax_response->set_html( $html );
		$ajax_response->set_login_result( $login_result );
		$this->apply_invoice_and_phone_to_login_response( $ajax_response, $user_data_arr, $checkout_sync );

		$this->output_response( $ajax_response );
	}

	private function render_ajax_form( string $type ): string {
		$common = [
			'module_path' => simplyin()->get_plugin_url(),
			'form_type'   => $type,
			'form_action' => admin_url( 'admin-ajax.php' ),
		];

		if ( 'delivery' === $type ) {
			return $this->render_delivery_form();
		}

		return $this->get_html(
			"{$type}_form.php",
			$common
		);
	}

	protected function render_delivery_form( ?array $api_payload = null ): string {
		if ( empty( $api_payload ) ) {
			$api_payload = $this->user_data_commerce_ajax_action();
		}
		if ( isset( $api_payload['code'] ) && 'INTERNAL_SERVER_ERROR' === $api_payload['code'] ) {
			wp_send_json_error( [ 'error' => 'Session expired' ] );
		}

		if ( ! isset( $api_payload['data'] ) && isset( $api_payload['userData'] ) ) {
			$api_payload['data'] = $api_payload['userData'];
		}
		$user = $api_payload['data'] ?? [];

		$context = [
			'module_path'        => simplyin()->get_plugin_url(),
			'carriers_cost'      => WC()->cart->get_cart_contents_total(),
			'_id'                => $user['_id'] ?? '',
			'name'               => $user['name'] ?? '',
			'surname'            => $user['surname'] ?? '',
			'billing_addresses'  => $this->parseAddress(
				$user['billingAddresses'] ?? [],
				$this->getSelectedBillingAddress()
			),
			'shipping_addresses' => $this->parseAddress(
				$user['shippingAddresses'] ?? [],
				$this->getSelectedShippingAddress()
			),
			'parcel_lockers'     => $this->getParcelLockers(
				$api_payload,
				$this->getSelectedParcel()
			),
			'service_type'       => $this->getServiceType(),
			'delivery_address'   => $this->getSelectedShippingAddress(),
			'billing_address'    => $this->getSelectedBillingAddress(),
			'selected_parcel'    => $this->getSelectedParcel(),
		];

		ob_start();
		simplyin()->locate_template(
			'delivery_form.php',
			[ 'args' => $context ],
			'src/Views/Widget'
		);

		return ob_get_clean();
	}


	public function getParcelLockers( $data, $selected = false ) {
		$mapping            = new Mapping();
		$available_carriers = $mapping->get_available_providers( true );

		if ( empty( $available_carriers ) ) {
			return [];
		}

		$parcel_lockers = $data['data']['parcelLockers'];
		if ( ! is_array( $parcel_lockers ) ) {
			return [];
		}

		foreach ( $parcel_lockers as $key => $parcel ) {
			if ( ! isset( $parcel['providerName'] ) || ! in_array(
				$parcel['providerName'],
				$available_carriers
			) ) {
				unset( $parcel_lockers[ $key ] );
				continue;
			}
			if ( empty( $parcel['addressName'] ) ) {
				$parcel_lockers[ $key ]['addressName'] = $parcel['lockerId'];
			}
			$parcel_lockers[ $key ]['address_desc'] = $parcel['desc'] ?? '';
			if ( $parcel['_id'] === $selected || ( false === $selected && 'parcel_machine' === ( $parcel['service_type'] ?? '' ) ) ) {
				$parcel_lockers[ $key ]['selected'] = true;
				$selected                           = $parcel['_id'];
			} else {
				$parcel_lockers[ $key ]['selected'] = false;
			}
		}
		$return = array_values( $parcel_lockers );

		$order_by_data = get_option( 'SimplyInLogisticsPartnersOrder' );

		if ( ! empty( $return ) && is_array( $order_by_data ) && ! empty( $order_by_data ) ) {
			$return = $this->sortItemsByProvider( $return, $order_by_data );
		}

		return $return;
	}


	/**
	 * Sorts $items according to the priority defined in $orderBy.
	 *
	 * @param array $items The unsorted list of items (each item must contain a 'providerName' key).
	 * @param array $orderBy An indexed array that contains provider names in the desired order.
	 *
	 * @return array Sorted array of items.
	 */
	private function sortItemsByProvider(
		array $items,
		array $orderBy
	): array {
		// 1. Build a lookup table: providerName => priority index (0‑based)
		$priority = [];
		foreach ( $orderBy as $idx => $provider ) {
			// Normalise the provider name – e.g. case‑insensitive match.
			// If you need exact matching, remove strtolower().
			$priority[ strtolower( $provider ) ] = $idx;
		}

		// 2. Separate items into two buckets:
		// - $ordered   : items that have a defined priority
		// - $unmatched : items whose provider is not in the order list
		$ordered   = [];
		$unmatched = [];

		foreach ( $items as $item ) {
			if ( ! isset( $item['providerName'] ) ) {
				// If an item has no providerName, treat it as unmatched.
				$unmatched[] = $item;
				continue;
			}

			$key = strtolower( $item['providerName'] );

			if ( array_key_exists( $key, $priority ) ) {
				// Store the priority so we can sort later
				$ordered[ $priority[ $key ] ][] = $item;
			} else {
				$unmatched[] = $item;
			}
		}

		// 3. Merge buckets in order of priority.
		// Items that share the same priority keep their original relative order.
		ksort( $ordered );          // Ensure priorities are processed from lowest to highest
		$sorted = [];
		foreach ( $ordered as $bucket ) {
			foreach ( $bucket as $itm ) {
				$sorted[] = $itm;
			}
		}

		// 4. Append the unmatched items at the end.
		return array_merge( $sorted, $unmatched );
	}


	public function parseAddress( $addresses, $selected ) {
		if ( empty( $addresses ) || ! is_array( $addresses ) ) {
			return [];
		}

		foreach ( $addresses as $key => $address ) {
			$addresses[ $key ]['selected'] = false;
		}

		foreach ( $addresses as $key => $address ) {
			if ( ( isset( $address['_id'] ) && $address['_id'] === $selected )
				|| false === $selected ) {
				$addresses[ $key ]['selected'] = true;
				break;
			}
		}

		return $addresses;
	}

	public function getSelectedShippingAddress() {
		$shipping = $this->simplyin_session->get( 'simplyin_shipping', '' );
		if ( ! empty( $shipping ) ) {
			return $shipping;
		}

		return false;
	}

	public function getSelectedParcel() {
		$parcel = $this->simplyin_session->get( 'simplyin_parcel', '' );
		if ( ! empty( $parcel ) ) {
			return $parcel;
		}

		return false;
	}

	public function getSelectedBillingAddress() {
		$billing = $this->simplyin_session->get( 'simplyin_billing', '' );
		if ( ! empty( $billing ) ) {
			return $billing;
		}

		return false;
	}

	public function getServiceType() {
		$service_type = $this->simplyin_session->get( 'service_type', '' );
		if ( ! empty( $service_type ) ) {
			return $service_type;
		}

		return false;
	}

	public function delete_address_ajax_action(): void {

		if ( check_ajax_referer(
			'simplyin_ajax',
			'ajax_token',
			false
		) ) {

			if ( isset( $_POST['id'] ) ) {
				$address_id = sanitize_text_field( wp_unslash( $_POST['id'] ) );
			} else {
				$this->output_response_bad_request();
			}
		} else {
			$this->output_response_unauthorized();
		}

		$api_payload = $this->user_data_commerce_ajax_action();
		$user_data   = $api_payload['data'] ?? $api_payload['userData'] ?? [];

		$address_type = '';
		foreach (
			[
				'billingAddresses',
				'shippingAddresses',
				'parcelLockers',
			] as $section
		) {
			foreach ( $user_data[ $section ] ?? [] as $id => $row ) {
				if ( ( $row['_id'] ?? '' ) === $address_id ) {
					unset( $user_data[ $section ][ $id ] );
					$user_data[ $section ] = array_values( $user_data[ $section ] );
					$address_type          = $section;
					break 2;
				}
			}
		}

		$response = new Delete_Address_Response();

		if ( '' === $address_type ) {
			$response->set_is_success( false );
			$response->set_error( 'ADDRESS_NOT_FOUND' );
			$this->output_response( $response );

			return;
		}

		$api          = simplyin()->get_api();
		$auth_token   = $this->getAuthToken();
		$api_response = $api->sendRequest(
			'userData',
			'PATCH',
			[ 'userData' => $user_data ],
			[ 'api_token' => $auth_token ]
		);

		$this->simplyin_session->set( 'simplyin_user_data_commerce', null );
		$this->simplyin_session->set(
			'simplyin_user_data_commerce_timestamp',
			null
		);

		$updated = json_decode(
			json_encode( $api_response->data->$address_type ?? [] ),
			true
		);

		$response->set_is_success( true );
		$response->set_address_type( $address_type );
		$response->set_addresses( $updated );

		$logger = simplyin()->get_simplyin_logger();
		$logger->log_if_wpdebug_on(
			'[delete_address] OUTPUT ' . $this->print_r(
				$response->to_array(),
				true
			),
			'widget_ajax'
		);
		$this->output_response( $response );
	}


	public function findAddress( $data, $id ) {
		$data         = isset( $data['data'] ) ? $data['data'] : $data['userData'];
		$address_type = '';
		$address      = false;
		foreach ( $data['billingAddresses'] as $item ) {
			if ( $id === $item['_id'] ) {
				$address      = $item;
				$address_type = 'billingAddresses';
			}
		}

		foreach ( $data['shippingAddresses'] as $item ) {
			if ( $id === $item['_id'] ) {
				$address      = $item;
				$address_type = 'shippingAddresses';
			}
		}

		foreach ( $data['parcelLockers'] as $item ) {
			if ( $id === $item['_id'] ) {
				$address      = $item;
				$address_type = 'parcelLockers';
			}
		}

		return [
			'address'      => $address,
			'address_type' => $address_type,
		];
	}

	public function getDefaultCountryIso(): string {
		return WC()->countries->get_base_country();
	}

	public function edit_address_ajax_action(): void {

		if ( check_ajax_referer(
			'simplyin_ajax',
			'ajax_token',
			false
		) ) {

			if ( isset( $_POST['id'] ) ) {
				$address_id = sanitize_text_field( wp_unslash( $_POST['id'] ) );
			} else {
				$this->output_response_bad_request();
			}
		} else {
			$this->output_response_unauthorized();
		}
		$logger = simplyin()->get_simplyin_logger();

		$api_payload   = $this->user_data_commerce_ajax_action();
		$address_entry = $this->api_find_address(
			$api_payload,
			$address_id,
			true
		);

		$response = new Edit_Address_Response();

		if ( ! $address_entry ) {
			$response->set_is_success( false );
			$response->set_error( 'ADDRESS_NOT_FOUND' );
			$this->output_response( $response );

			return;
		}

		$countries_list = WC()->countries->get_countries();
		$default_iso    = WC()->countries->get_base_country();

		$html = $this->get_html(
			'edit_address_form.php',
			[
				'address'             => $address_entry['address'],
				'address_type'        => $address_entry['address_type'],
				'countries'           => $countries_list,
				'default_country_iso' => $default_iso,
				'module_path'         => simplyin()->get_plugin_url(),
			]
		);

		$response->set_is_success( true );
		$response->set_html( $html );

		$logger->log_if_wpdebug_on(
			'[edit_address] OUTPUT ' . $this->print_r(
				$response->to_array(),
				true
			),
			'widget_ajax'
		);
		$this->output_response( $response );
	}

	public function create_empty_address( $address_type ) {
		return [
			'address_type' => $address_type,
			'address'      => [
				'_id'         => '',
				'name'        => '',
				'surname'     => '',
				'street'      => '',
				'postalCode'  => '',
				'city'        => '',
				'companyName' => '',
				'addressName' => '',
				'taxId'       => '',
				'country'     => $this->getDefaultCountryIso(),
			],
		];
	}

	public function add_address_ajax_action() {

		$logger = simplyin()->get_simplyin_logger();

		if ( check_ajax_referer(
			'simplyin_ajax',
			'ajax_token',
			false
		) ) {

			if ( isset( $_POST['address_type'] ) ) {
				$address_type = sanitize_text_field( wp_unslash( $_POST['address_type'] ) );
			} else {
				$this->output_response_bad_request();
			}
		} else {
			$this->output_response_unauthorized();
		}

		$ajax_response = new Add_Address_Response();

		if ( ! $address_type ) {
			$ajax_response->set_is_success( false );
			$ajax_response->set_error( 'ADDRESS_TYPE_NOT_FOUND' );
			$this->output_response( $ajax_response );
		}

		$countries_list = WC()->countries->get_countries();
		$default_iso    = WC()->countries->get_base_country();
		$address_data   = $this->create_empty_address( $address_type );

		if ( 'parcel' !== $address_type ) {
			$html = $this->get_html(
				'edit_address_form.php',
				[
					'address'             => $address_data['address'],
					'address_type'        => $address_data['address_type'],
					'countries'           => $countries_list,
					'default_country_iso' => $default_iso,
					'module_path'         => simplyin()->get_plugin_url(),
				]
			);
		} else {
			$pickup_point_kind = '';
			if ( isset( $_POST['pickup_point_kind'] ) ) {
				$pickup_point_kind = sanitize_text_field(
					wp_unslash( $_POST['pickup_point_kind'] )
				);
			}
			$pickup_point_kind = Closest_Parcel_Lockers_Request_Builder::normalize_service_type(
				'' !== $pickup_point_kind ? $pickup_point_kind : null
			);

			$html = $this->get_html(
				'autocomplete_form.php',
				[
					'module_path'         => simplyin()->get_plugin_url(),
					'pickup_point_kind'   => $pickup_point_kind ?? '',
				]
			);
		}

		$ajax_response->set_is_success( true );
		$ajax_response->set_html( $html );

		if ( check_ajax_referer(
			'simplyin_ajax',
			'ajax_token',
			false
		) ) {
			$logger->log_if_wpdebug_on(
				sprintf(
					'[Widget_Controller] [add_address_ajax_action] INPUT %s',
					$this->print_r(
						[
							'ajax_response' => $ajax_response,
						],
						true
					)
				),
				'widget_ajax'
			);
		} else {
			$this->output_response_unauthorized();
		}

		$this->output_response( $ajax_response );
	}

	private function validate_address( array $address_in ) {
		if ( 'parcelLockers' === ( $address_in['address_type'] ?? '' ) ) {
			return true;
		}

		$errors = [];

		$first_name = $address_in['name'] ?? '';
		$last_name  = $address_in['surname'] ?? '';

		$person_regex = '/^[\p{L}\p{M}\s\'\-]{1,255}$/u';

		if ( ! preg_match( $person_regex, $first_name ) ) {
			$errors['name'] = __( 'Invalid first name', 'simplyin' );
		}
		if ( ! preg_match( $person_regex, $last_name ) ) {
			$errors['surname'] = __( 'Invalid last name', 'simplyin' );
		}

		$company_name = $address_in['companyName'] ?? '';
		$label_name   = $address_in['addressName'] ?? '';
		$tax_id       = $address_in['taxId'] ?? '';

		$generic_rx = '/^[\p{L}\p{M}\d\.\,\-\'\s]{0,255}$/u';

		if ( '' !== $company_name && ! preg_match(
			$generic_rx,
			$company_name
		) ) {
			$errors['companyName'] = __(
				'Invalid company name',
				'simplyin'
			);
		}
		if ( '' !== $label_name && ! preg_match( $generic_rx, $label_name ) ) {
			$errors['addressName'] = __(
				'Invalid address label',
				'simplyin'
			);
		}
		if ( '' !== $tax_id && ! preg_match(
			'/^[\p{Nd}A-Za-z\-\s]{3,32}$/u',
			$tax_id
		) ) {
			$errors['taxId'] = __( 'Invalid tax ID', 'simplyin' );
		}

		$street = $address_in['street'] ?? '';
		$city   = $address_in['city'] ?? '';

		if ( '' === $street || mb_strlen( $street ) > 255 ) {
			$errors['street'] = __( 'Invalid street', 'simplyin' );
		}
		if ( ! preg_match( '/^[\p{L}\p{M}\s\'\-]{1,255}$/u', $city ) ) {
			$errors['city'] = __( 'Invalid city', 'simplyin' );
		}

		$country_iso = strtoupper( $address_in['country'] ?? '' );
		$postcode    = $address_in['postalCode'] ?? '';

		if ( ! class_exists( '\WC_Validation' ) ) {
			require_once WC_ABSPATH . 'includes/class-wc-validation.php';
		}

		if ( ! \WC_Validation::is_postcode( $postcode, $country_iso ) ) {
			$errors['postalCode'] = __(
				'Invalid postal code for selected country',
				'simplyin'
			);
		}

		return empty( $errors ) ? true : $errors;
	}

	private function save_existing_address(
		array $address,
		string $id
	): array {

		$is_valid = $this->validate_address( $address );
		if ( true !== $is_valid ) {
			return [
				'is_valid' => $is_valid,
				'success'  => false,
			];
		}

		$api_payload = $this->user_data_commerce_ajax_action();
		$user_data   = [ 'userData' => $api_payload['data'] ];

		foreach ( [ 'billingAddresses', 'shippingAddresses' ] as $section ) {
			foreach ( $user_data['userData'][ $section ] as $k => $item ) {
				if ( $id === $item['_id'] ) {
					foreach ( $address as $key => $val ) {
						if ( array_key_exists( $key, $item ) ) {
							$user_data['userData'][ $section ][ $k ][ $key ] = $val;
						}
					}
					break 2;
				}
			}
		}

		$api   = simplyin()->get_api();
		$token = $this->getAuthToken();

		$patched = $api->sendRequest(
			'userData',
			'PATCH',
			$user_data,
			[ 'api_token' => $token ]
		);

		$this->simplyin_session->set( 'simplyin_user_data_commerce', null );
		$this->simplyin_session->set(
			'simplyin_user_data_commerce_timestamp',
			null
		);

		$address_data = $this->api_find_address(
			json_decode( json_encode( $patched ), true ),
			$id,
			true
		);

		return [
			'address'  => $address_data['address'] ?? $address,
			'is_valid' => true,
			'success'  => true,
		];
	}

	private function create_new_address( array $address ): array {

		$is_valid = $this->validate_address( $address );
		if ( true !== $is_valid ) {
			return [
				'is_valid' => $is_valid,
				'success'  => false,
			];
		}

		$api_payload  = $this->user_data_commerce_ajax_action();
		$user_data    = [ 'userData' => $api_payload['data'] ];
		$address_type = $address['address_type'];

		// ID-based idempotency: if payload contains _id that already exists in target list, reuse and do not append
		$incoming_id = isset( $address['_id'] ) ? (string) $address['_id'] : '';
		if ( '' !== $incoming_id ) {
			foreach ( (array) ( $user_data['userData'][ $address_type ] ?? [] ) as $existing ) {
				if ( isset( $existing['_id'] ) && (string) $existing['_id'] === $incoming_id ) {
					if ( 'parcelLockers' === $address_type ) {
						$this->remember_parcel_locker_selection( $existing );
					}
					return $this->create_new_address_duplicate_response( $existing, $address_type );
				}
			}
		}

		// Content-based idempotency for billing/shipping: detect duplicate when _id is absent (new address form)
		if ( in_array( $address_type, [ 'billingAddresses', 'shippingAddresses' ], true ) ) {
			$fields = [ 'name', 'surname', 'street', 'city', 'postalCode' ];
			foreach ( (array) ( $user_data['userData'][ $address_type ] ?? [] ) as $existing ) {
				$match = true;
				foreach ( $fields as $field ) {
					if ( strtolower( trim( (string) ( $address[ $field ] ?? '' ) ) )
						!== strtolower( trim( (string) ( $existing[ $field ] ?? '' ) ) ) ) {
						$match = false;
						break;
					}
				}
				if ( $match ) {
					return $this->create_new_address_duplicate_response( $existing, $address_type );
				}
			}
		}

		// For parcel lockers: also check by lockerId + provider (in case _id differs between API contexts)
		$is_parcel_locker = ( 'parcelLockers' === $address_type );
		if ( $is_parcel_locker ) {
			$incoming_locker_id = isset( $address['lockerId'] ) ? trim( (string) $address['lockerId'] ) : '';
			$incoming_provider  = strtolower(
				trim( (string) ( $address['label'] ?? $address['providerName'] ?? '' ) )
			);
			if ( '' !== $incoming_locker_id && '' !== $incoming_provider ) {
				foreach ( (array) ( $user_data['userData']['parcelLockers'] ?? [] ) as $existing ) {
					$existing_locker_id = isset( $existing['lockerId'] ) ? trim( (string) $existing['lockerId'] ) : '';
					$existing_provider  = strtolower(
						trim( (string) ( $existing['providerName'] ?? $existing['label'] ?? '' ) )
					);
					if ( '' !== $existing_locker_id && '' !== $existing_provider
						&& $incoming_locker_id === $existing_locker_id
						&& $incoming_provider === $existing_provider ) {
						$this->remember_parcel_locker_selection( $existing );
						return $this->create_new_address_duplicate_response( $existing, $address_type );
					}
				}
			}
		}

		$address_for_api = $address;
		if ( $is_parcel_locker ) {
			$address_for_api = $this->normalize_parcel_locker_payload( $address_for_api );
		}

		if ( ! isset( $user_data['userData'][ $address_type ] ) || ! is_array( $user_data['userData'][ $address_type ] ) ) {
			$user_data['userData'][ $address_type ] = [];
		}

		$user_data['userData'][ $address_type ][] = $address_for_api;

		$api   = simplyin()->get_api();
		$token = $this->getAuthToken();

		$patched = $api->sendRequest(
			'userData',
			'PATCH',
			$user_data,
			[ 'api_token' => $token ]
		);

		$this->simplyin_session->set( 'simplyin_user_data_commerce', null );
		$this->simplyin_session->set(
			'simplyin_user_data_commerce_timestamp',
			null
		);

		$list    = json_decode(
			json_encode( $patched->data->$address_type ?? [] ),
			true
		);
		$created = end( $list );

		if ( is_array( $created ) && ! isset( $created['address_type'] ) ) {
			$created['address_type'] = $address_type;
		}

		if ( $is_parcel_locker && is_array( $created ) ) {
			$this->remember_parcel_locker_selection( $created );
		}

		return [
			'add_address'  => $created,
			'address_type' => $address_type,
			'html'         => $this->render_delivery_form(),
			'is_valid'     => true,
			'success'      => true,
		];
	}

	/**
	 * Returns the response for a duplicate address (already in list).
	 * Used when _id or lockerId+provider match is found for parcel lockers.
	 *
	 * @param array  $existing     The existing address/parcel from userData.
	 * @param string $address_type 'parcelLockers', 'billingAddresses', or 'shippingAddresses'.
	 * @return array Response array with html and success.
	 */
	private function create_new_address_duplicate_response( array $existing, string $address_type ): array {
		return [
			'add_address'  => $existing,
			'address_type' => $address_type,
			'html'         => $this->render_delivery_form(),
			'is_duplicate' => true,
			'is_valid'     => true,
			'success'      => true,
		];
	}

	private function normalize_parcel_locker_payload( array $parcel ): array {
		if ( isset( $parcel['address_type'] ) ) {
			unset( $parcel['address_type'] );
		}

		if ( ! isset( $parcel['providerName'] ) && isset( $parcel['label'] ) ) {
			$parcel['providerName'] = (string) $parcel['label'];
		}

		if ( empty( $parcel['addressName'] ) && isset( $parcel['lockerId'] ) ) {
			$parcel['addressName'] = (string) $parcel['lockerId'];
		}

		return $parcel;
	}

	private function remember_parcel_locker_selection( array $parcel ): void {
		$this->simplyin_session->set( 'simplyin_parcel_locker_full', $parcel );

		if ( isset( $parcel['_id'] ) && '' !== $parcel['_id'] ) {
			$this->simplyin_session->set(
				'simplyin_parcel',
				(string) $parcel['_id']
			);
		}

		if ( isset( $parcel['lockerId'] ) && '' !== $parcel['lockerId'] ) {
			$this->simplyin_session->set(
				'simplyin_parcel_locker_id',
				(string) $parcel['lockerId']
			);
		}

		if ( isset( $parcel['providerName'] ) && '' !== $parcel['providerName'] ) {
			$this->simplyin_session->set(
				'simplyin_parcel_provider',
				(string) $parcel['providerName']
			);
		}
	}

	public function submit_address_ajax_action(): void {

		$logger = simplyin()->get_simplyin_logger();

		if ( check_ajax_referer(
			'simplyin_ajax',
			'ajax_token',
			false
		) ) {

			if ( isset( $_POST['address'] ) && is_array( $_POST['address'] ) ) {
				$raw_address = array_map(
					'sanitize_text_field',
					wp_unslash( $_POST['address'] )
				);
			} else {
				$raw_address = [];
			}

			if ( isset( $_POST['parcel'] ) && is_array( $_POST['parcel'] ) ) {
				$raw_parcel = array_map(
					'sanitize_text_field',
					wp_unslash( $_POST['parcel'] )
				);
			} else {
				$raw_parcel = [];
			}
		} else {
			$this->output_response_unauthorized();
		}

		$payload   = $raw_address ?: $raw_parcel;
		$is_parcel = ! empty( $raw_parcel );

		$logger->log_if_wpdebug_on(
			'[submit_address] INPUT ' . $this->print_r(
				[
					'is_parcel' => $is_parcel,
					'payload'   => $payload,
				],
				true
			),
			'widget_ajax'
		);

		$response = new Submit_Address_Response();

		if ( empty( $payload ) ) {
			$response->set_is_success( false );
			$response->set_error( 'INVALID_ADDRESS' );
			$this->output_response( $response );
		}

		$address = is_string( $payload ) ? json_decode(
			$payload,
			true
		) : $payload;
		if ( ! is_array( $address ) ) {
			$response->set_is_success( false );
			$response->set_error( 'BAD_FORMAT' );
			$this->output_response( $response );
		}

		$id     = $address['_id'] ?? '';
		$result = ( $is_parcel || '' === $id )
			? $this->create_new_address( $address )
			: $this->save_existing_address( $address, $id );

		$response->set_is_success( true );
		$response->set_result( $result );

		$logger->log_if_wpdebug_on(
			'[submit_address] OUTPUT ' . $this->print_r(
				$response->to_array(),
				true
			),
			'widget_ajax'
		);
		$this->output_response( $response );
	}


	public function search_address_ajax_action(): void {

		if ( check_ajax_referer(
			'simplyin_ajax',
			'ajax_token',
			false
		) ) {

			if ( isset( $_POST['address'] ) ) {
				$address = sanitize_text_field( wp_unslash( $_POST['address'] ) );
			} else {
				$this->output_response_bad_request();
			}
		} else {
			$this->output_response_unauthorized();
		}
		$query = $address;

		$api          = simplyin()->get_api();
		$auth_token   = $this->getAuthToken();
		$api_response = $api->sendRequest(
			'addresses/find',
			'POST',
			[ 'searchAddressBy' => $query ],
			[ 'api_token' => $auth_token ]
		);

		$response = new Search_Address_Response();

		if ( ! is_object( $api_response ) || ! empty( $api_response->code ) ) {

			$response->set_is_success( false );
			$response->set_error( $api_response->code ?? 'API_ERROR' );
			$this->output_response( $response );

			return;
		}

		$addresses = json_decode(
			json_encode( $api_response->data ?? [] ),
			true
		);

		$response->set_is_success( true );
		$response->set_addresses( $addresses );

		$this->output_response( $response );
	}


	public function getAuthToken() {
		return $this->simplyin_session->get( 'simplyin_auth_token', false );
	}

	public function get_closest_parcel_lockers_ajax_action(): void {

		$logger = simplyin()->get_simplyin_logger();

		if ( check_ajax_referer(
			'simplyin_ajax',
			'ajax_token',
			false
		) ) {

			if ( isset( $_POST['coordinates'] ) ) {
				if ( is_array( $_POST['coordinates'] ) ) {
					$coordinates = array_map(
						'sanitize_text_field',
						wp_unslash( $_POST['coordinates'] )
					);
				} else {
					$coordinates = sanitize_text_field( wp_unslash( $_POST['coordinates'] ?? [] ) );
					if ( is_string( $coordinates ) ) {
						$coordinates = json_decode( $coordinates, true ) ?: [];
					}
				}
			} else {
				$coordinates = [];
			}

			$lat    = isset( $coordinates['lat'] ) ? (float) $coordinates['lat'] : 0.0;
			$lng    = isset( $coordinates['lng'] ) ? (float) $coordinates['lng'] : 0.0;
			$radius = isset( $_POST['radius'] ) ? (int) sanitize_text_field( wp_unslash( $_POST['radius'] ) ) : 5000;
			$limit  = isset( $_POST['limit'] ) ? (int) sanitize_text_field( wp_unslash( $_POST['limit'] ) ) : 20;

			$pickup_point_kind_raw = '';
			if ( isset( $_POST['pickup_point_kind'] ) ) {
				$pickup_point_kind_raw = sanitize_text_field(
					wp_unslash( $_POST['pickup_point_kind'] )
				);
			}
			$closest_service_type = Closest_Parcel_Lockers_Request_Builder::normalize_service_type(
				'' !== $pickup_point_kind_raw ? $pickup_point_kind_raw : null
			);
		} else {
			$this->output_response_unauthorized();
		}

		$logger->log_if_wpdebug_on(
			'[get_closest_parcel_lockers] INPUT ' . $this->print_r(
				[
					'lat'                 => $lat,
					'lng'                 => $lng,
					'radius'              => $radius,
					'limit'               => $limit,
					'pickup_point_kind'   => $closest_service_type,
				],
				true
			),
			'widget_ajax'
		);

		$api       = simplyin()->get_api();
		$authToken = $this->getAuthToken();

		$mapping   = new Mapping();
		$providers = $mapping->get_available_providers( true );

		$body = Closest_Parcel_Lockers_Request_Builder::build_body(
			$lat,
			$lng,
			$radius,
			$limit,
			$providers,
			$closest_service_type
		);

		$api_response = $api->sendRequest(
			'parcelLockers/getClosest',
			'POST',
			$body,
			[ 'api_token' => $authToken ]
		);

		$response = new Closest_Parcel_Lockers_Response();

		if ( ! is_object( $api_response ) || ! empty( $api_response->code ) ) {
			$response->set_is_success( false );
			$response->set_error( $api_response->code ?? 'API_ERROR' );
			$this->output_response( $response );

			return;
		}

		$points = json_decode( json_encode( $api_response->data ?? [] ), true );

		$response->set_is_success( true );
		$response->set_points( $points );

		$logger->log_if_wpdebug_on(
			'[get_closest_parcel_lockers] OUTPUT ' . $this->print_r(
				$response->to_array(),
				true
			),
			'widget_ajax'
		);
		$this->output_response( $response );
	}


	private function login_customer_by_email( string $email, $api_response ): array {
		$user_data = $api_response->userData ?? null;
		$firstname = isset( $user_data->name ) ? (string) $user_data->name : '';
		$lastname  = isset( $user_data->surname ) ? (string) $user_data->surname : '';

		$wp_user = get_user_by( 'email', $email );
		if ( ! $wp_user ) {
			return [ 'is_guest' => true, 'wp_logged_in' => false, 'firstname' => $firstname, 'lastname' => $lastname ];
		}

		if ( is_user_logged_in() ) {
			return [ 'is_guest' => false, 'wp_logged_in' => false, 'firstname' => $firstname, 'lastname' => $lastname ];
		}

		// Anti-takeover: log the browser into the shop's native WP/WooCommerce account ONLY when
		// the backend confirmed the shopper owns the email (verified email). Proving control of
		// the checkout OTP authorizes payment but must NOT by itself grant the email owner's shop
		// account. Absent confirmation (older backend that does not send it) is treated as NOT
		// allowed = fail safe: the shopper proceeds as a guest and payment still works via the
		// stored token.
		if ( true !== ( $api_response->shopLoginAllowed ?? null ) ) {
			// A WC account exists for this email but the backend did not confirm the shopper owns
			// it. When the backend explicitly says shopLoginAllowed === false (email not verified),
			// offer the email-OTP challenge so the real owner can verify a code we e-mail them and
			// log in. When the flag is ABSENT (older backend without the OTP endpoints) fail safe:
			// a plain guest, no challenge — otherwise we would send the shopper to a challenge whose
			// send-OTP endpoint does not exist. The no-account case above carries no flag either.
			$challenge_available = false === ( $api_response->shopLoginAllowed ?? null );
			return [ 'is_guest' => true, 'wp_logged_in' => false, 'challenge_available' => $challenge_available, 'firstname' => $firstname, 'lastname' => $lastname ];
		}

		// Scenario 4: guest with existing WP account AND verified email — log in.
		// wc_set_customer_auth_cookie() internally calls wp_set_current_user() + wp_set_auth_cookie() + session->init_session_cookie().
		wc_set_customer_auth_cookie( $wp_user->ID );
		do_action( 'wp_login', $wp_user->user_login, $wp_user ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- core WP authentication hook

		return [ 'is_guest' => false, 'wp_logged_in' => true, 'firstname' => $firstname, 'lastname' => $lastname ];
	}

	/**
	 * Invalidates any previously stored Simply.IN session tokens (and the payment-methods cache
	 * keyed by them). Used whenever the shopper switches to a different e-mail address, so the
	 * next order can't be created against the previous account.
	 */
	private function invalidate_simplyin_session_tokens(): void {
		$session             = $this->simplyin_session;
		$previous_token      = (string) $session->get( 'simplyin_auth_token', '' );
		$previous_session_id = (string) $session->get( 'id', '' );
		if ( '' !== $previous_token || '' !== $previous_session_id ) {
			Payment_Fields::clear_methods_cache( $previous_token, $previous_session_id );
		}
		$session->set( 'simplyin_auth_token', '' );
		$session->set( 'simplyin_refresh_token', '' );
		$session->set( 'simplyin_notification_token_id', '' );
		$session->set( 'simplyin_push_confirmed', false );
		$session->set( 'simplyin_pending_email', '' );
		// Drop the pinned email-first challenge target too: in by-email mode verify falls back to
		// this value when no auth token is present, so a stale pin from a previous e-mail must not
		// survive an e-mail switch/logout.
		$session->set( 'simplyin_challenge_email', '' );
		// The create-account decision belongs to the previous e-mail's checkout attempt — a stale
		// 'on' surviving an e-mail switch could silently create an unwanted account.
		$session->set( 'simplyin_create_account', '' );
	}

	private function get_shop_login_challenge_html( string $code_sent_status_message = '' ): string {
		$message = __(
			'Enter the 6-digit code we e-mailed you to log in to your shop account.',
			'simplyin'
		);
		return $this->get_html(
			'shop_login_challenge_form.php',
			[
				'message'               => $message,
				'codeSentStatusMessage' => $code_sent_status_message,
			]
		);
	}

	/**
	 * Shop-login email-OTP challenge — STEP 1.
	 *
	 * Two strictly separate modes, mirroring the backend contract:
	 * - TOKEN mode: a checkout authToken is stored for the typed e-mail →
	 *   the backend targets the authenticated user's own e-mail, no e-mail is sent on the wire.
	 * - BY-EMAIL mode: no usable authToken → precheck LOCALLY that a WC
	 *   account exists for the typed e-mail and the browser isn't already logged in, then ask the
	 *   backend to challenge that e-mail under merchant-key trust (NO api_token). Every non-ok
	 *   outcome (no WC account, no backend account, e-mail already verified, backend error) returns
	 *   challenge_started=false so the front end falls back to the regular SMS flow.
	 */
	public function send_shop_login_email_otp_ajax_action(): void {
		if ( ! check_ajax_referer( 'simplyin_ajax', 'ajax_token', false ) ) {
			$this->output_response_unauthorized();
		}

		$post_email = isset( $_POST['email'] ) ? sanitize_email( wp_unslash( $_POST['email'] ) ) : '';
		$auth_token = (string) $this->getAuthToken();
		$response   = new Send_Shop_Login_Email_Otp_Response();

		// These two calls build their body separately (no plugin/shop metadata needed), so
		// only language is added by hand here — via the same shared derivation
		// update_array_with_plugin_info() uses, to avoid it drifting from the backend contract.
		$language = $this->get_shop_language();

		$is_resend = isset( $_POST['isResend'] ) && in_array(
			sanitize_text_field( wp_unslash( $_POST['isResend'] ) ),
			[ '1', 'true', 'yes', 'on' ],
			true
		);

		$api            = simplyin()->get_api();
		$merchant_token = get_option( 'simplyin_api_key' );

		// TOKEN mode only when the stored token belongs to the typed e-mail (no e-mail posted =
		// always token mode). A token for a DIFFERENT e-mail must not be
		// used to challenge the new address — that falls through to by-email mode below.
		$session_email = (string) $this->simplyin_session->get( 'simplyin_email', '' );
		$token_mode    = '' !== $auth_token
			&& ( '' === $post_email || 0 === strcasecmp( $post_email, $session_email ) );

		// Local prechecks apply to BOTH modes: the challenge can only ever log the shopper into an
		// existing WC account, so neither token nor by-email mode may start one that the verify step
		// could never convert (already logged in, or no WC account for the targeted e-mail).
		$target = ( '' !== $post_email ) ? $post_email : $session_email;

		if ( is_user_logged_in() ) {
			$response->set_is_success( true );
			$response->set_challenge_started( false );
			$response->set_fallback_reason( 'WP_ALREADY_LOGGED_IN' );
			$this->output_response( $response );

			return;
		}

		if ( '' !== $target && ! get_user_by( 'email', $target ) ) {
			// Answered locally before any backend throttle: exposing the distinguishing reason would
			// leak WC-account existence to any visitor. Keep the specific reason in server-side logs
			// only; the client just sees challenge_started=false (generic, no fallback_reason).
			simplyin()->get_simplyin_logger()->log_if_wpdebug_on(
				'[Widget_Controller] [send_shop_login_email_otp] no WC account for challenge target (fallback_reason=NO_WC_ACCOUNT)'
			);
			$response->set_is_success( true );
			$response->set_challenge_started( false );
			$this->output_response( $response );

			return;
		}

		if ( $token_mode ) {
			$token_mode_body = [
				'apiKey'   => $merchant_token,
				'language' => $language,
			];
			if ( $is_resend ) {
				$token_mode_body['isResend'] = true;
			}
			$api_response = $api->sendRequest(
				'checkout/sendShopLoginEmailOtpCommerce',
				'POST',
				$token_mode_body,
				[ 'api_token' => $auth_token ]
			);

			if ( ! is_object( $api_response ) || empty( $api_response->ok ) ) {
				$error_code = is_object( $api_response ) ? ( $api_response->code ?? 'ERROR' ) : 'ERROR';
				if ( 'UNAUTHORIZED' === $error_code ) {
					simplyin()->handle_api_unauthorized();
				}
				$response->set_is_success( false );
				$response->set_error( $error_code );
				$this->output_response( $response );

				return;
			}

			$response->set_is_success( true );
			$response->set_show_popup( true );
			$response->set_challenge_started( true );
			$response->set_html( $this->get_shop_login_challenge_html( (string) ( $api_response->codeSentStatusMessage ?? '' ) ) );
			$this->output_response( $response );

			return;
		}

		// Compat: every call shape that got UNAUTHORIZED previously (no token, no e-mail)
		// keeps getting it.
		if ( '' === $post_email ) {
			$response->set_is_success( false );
			$response->set_error( 'UNAUTHORIZED' );
			$this->output_response( $response );

			return;
		}

		// BY-EMAIL mode. The is_user_logged_in / WC-account prechecks already ran above (both modes).
		// The session may still hold tokens for a previously identified different account —
		// invalidate them (same rule as submit_email_ajax_action) so a challenge for the new
		// e-mail can't ride on the old account's checkout session.
		$pending_email = (string) $this->simplyin_session->get( 'simplyin_pending_email', '' );
		if ( ( $session_email && 0 !== strcasecmp( $session_email, $post_email ) )
			|| ( $pending_email && 0 !== strcasecmp( $pending_email, $post_email ) ) ) {
			$this->invalidate_simplyin_session_tokens();
		}

		$by_email_body = [
			'email'    => $post_email,
			'apiKey'   => $merchant_token,
			'language' => $language,
		];
		if ( $is_resend ) {
			$by_email_body['isResend'] = true;
		}
		$api_response = $api->sendRequest(
			'checkout/sendShopLoginEmailOtpCommerce',
			'POST',
			$by_email_body
		);

		if ( is_object( $api_response ) && ! empty( $api_response->ok ) ) {
			// Pin the challenged e-mail server-side: the verify step must never trust the client.
			$this->simplyin_session->set( 'simplyin_challenge_email', $post_email );
			$response->set_is_success( true );
			$response->set_show_popup( true );
			$response->set_challenge_started( true );
			$response->set_html( $this->get_shop_login_challenge_html( (string) ( $api_response->codeSentStatusMessage ?? '' ) ) );
			$this->output_response( $response );

			return;
		}

		$code = is_object( $api_response ) ? (string) ( $api_response->code ?? '' ) : '';
		if ( 'UNAUTHORIZED' === $code ) {
			simplyin()->handle_api_unauthorized();
		}

		$fallback_reasons = [
			'NOT_FOUND'    => 'NO_SIN_ACCOUNT',
			'CONFLICT'     => 'EMAIL_VERIFIED',
			'UNAUTHORIZED' => 'UNAUTHORIZED',
		];

		$response->set_is_success( true );
		$response->set_challenge_started( false );
		$response->set_fallback_reason( $fallback_reasons[ $code ] ?? 'ERROR' );
		$this->output_response( $response );
	}

	/**
	 * Shop-login email-OTP challenge — STEP 2. Verifies the code.
	 *
	 * TOKEN mode: on success the backend flips emailVerified and returns fresh
	 * tokens + shopLoginAllowed=true, so we persist the tokens and log the browser into the WC
	 * account (login_customer_by_email now passes the gate).
	 *
	 * BY-EMAIL mode: no authToken → verify {email, code} under merchant-key
	 * trust. The e-mail is resolved from the session key pinned at send time — NEVER from the
	 * request. On success the backend returns shopLoginAllowed=true and the verified e-mail but NO
	 * auth tokens (email control alone must not mint a session); we only log the browser into
	 * the WC account, fail-closed on the BACKEND-returned e-mail.
	 *
	 * A wrong / expired code surfaces as isCodeValid=false so the shared pin UI shows the error.
	 */
	public function verify_shop_login_email_otp_ajax_action(): void {
		$code = '';
		if ( check_ajax_referer( 'simplyin_ajax', 'ajax_token', false ) ) {
			if ( isset( $_POST['code'] ) ) {
				$code = sanitize_text_field( wp_unslash( $_POST['code'] ) );
			} else {
				$this->output_response_bad_request();
			}
		} else {
			$this->output_response_unauthorized();
		}

		$email      = (string) $this->simplyin_session->get( 'simplyin_email', '' );
		$auth_token = (string) $this->getAuthToken();
		$response   = new Verify_Shop_Login_Email_Otp_Response();

		$api            = simplyin()->get_api();
		$merchant_token = get_option( 'simplyin_api_key' );

		if ( '' === $auth_token ) {
			// BY-EMAIL mode — only the e-mail pinned server-side at send time is ever verified.
			$challenge_email = (string) $this->simplyin_session->get( 'simplyin_challenge_email', '' );

			if ( '' === $challenge_email ) {
				$response->set_is_success( false );
				$response->set_is_code_valid( false );
				$this->output_response( $response );

				return;
			}

			$api_response = $api->sendRequest(
				'checkout/verifyShopLoginEmailOtpCommerce',
				'POST',
				[
					'email'  => $challenge_email,
					'code'   => $code,
					'apiKey' => $merchant_token,
				]
			);

			// FAIL-CLOSED: log into the WC account only when the backend confirmed ownership
			// (shopLoginAllowed === true) and only via the BACKEND-returned e-mail. Do NOT store
			// any SIN auth/refresh tokens here — the email-only proof must not create a SIN
			// checkout session.
			$backend_email = '';
			if ( is_object( $api_response ) ) {
				$backend_email = (string) ( $api_response->email ?? ( ( isset( $api_response->data ) && is_object( $api_response->data ) ) ? ( $api_response->data->email ?? '' ) : '' ) );
			}

			if ( is_object( $api_response )
				&& true === ( $api_response->shopLoginAllowed ?? null )
				&& '' !== $backend_email ) {
				$this->simplyin_session->set( 'simplyin_challenge_email', '' );

				$login_result = $this->login_customer_by_email( $backend_email, $api_response );

				$response->set_is_success( true );
				$response->set_is_code_valid( true );
				$response->set_login_result( $login_result );
				$this->output_response( $response );

				return;
			}

			$error_code = is_object( $api_response ) ? ( $api_response->code ?? '' ) : '';
			if ( 'TOO_MANY_REQUESTS' === $error_code ) {
				$response->set_is_success( false );
				$response->set_code( 'TOO_MANY_REQUESTS' );
				$response->set_message( (string) ( $api_response->message ?? '' ) );
				$this->output_response( $response );

				return;
			}

			if ( 'UNAUTHORIZED' === $error_code ) {
				simplyin()->handle_api_unauthorized();
			}

			$response->set_is_success( false );
			$response->set_is_code_valid( false );
			$this->output_response( $response );

			return;
		}

		$api_response = $api->sendRequest(
			'checkout/verifyShopLoginEmailOtpCommerce',
			'POST',
			[
				'code'   => $code,
				'apiKey' => $merchant_token,
			],
			[ 'api_token' => $auth_token ]
		);

		if ( is_object( $api_response ) && ! empty( $api_response->authToken ) && true === ( $api_response->shopLoginAllowed ?? null ) ) {
			$session_id = (string) $this->simplyin_session->get( 'id', '' );
			Payment_Fields::clear_methods_cache( $auth_token, $session_id );
			$this->simplyin_session->set( 'simplyin_auth_token', (string) $api_response->authToken );
			$this->simplyin_session->set( 'simplyin_refresh_token', (string) ( $api_response->refreshToken ?? '' ) );
			Payment_Fields::clear_methods_cache( (string) $api_response->authToken, $session_id );

			$login_result = $this->login_customer_by_email( $email, $api_response );

			$response->set_is_success( true );
			$response->set_is_code_valid( true );
			$response->set_login_result( $login_result );
			$this->output_response( $response );

			return;
		}

		if ( 'TOO_MANY_REQUESTS' === ( $api_response->code ?? '' ) ) {
			$response->set_is_success( false );
			$response->set_code( 'TOO_MANY_REQUESTS' );
			$response->set_message( (string) ( $api_response->message ?? '' ) );
			$this->output_response( $response );

			return;
		}

		if ( 'UNAUTHORIZED' === ( $api_response->code ?? '' ) ) {
			simplyin()->handle_api_unauthorized();
		}

		$response->set_is_success( false );
		$response->set_is_code_valid( false );
		$this->output_response( $response );
	}

	/**
	 * Persists the "create Simply.IN account" intent to the WC session on every checkbox toggle.
	 *
	 * The checkbox is injected into the WooCommerce order-review/#payment fragment, which WC
	 * re-renders on every updated_checkout — so the POST field can be silently dropped between
	 * check and submit, losing account creation with no error. simplyInOnOrderCreate::run() uses
	 * the submitted POST/meta first and falls back to this session value to recover the intent
	 * when the POST field was dropped.
	 */
	public function save_create_account_flag_ajax_action(): void {
		if ( ! check_ajax_referer( 'simplyin_ajax', 'ajax_token', false ) ) {
			$this->output_response_unauthorized();

			return;
		}

		$raw_flag       = isset( $_POST['create_account'] )
			? sanitize_text_field( wp_unslash( $_POST['create_account'] ) )
			: '';
		$create_account = '' !== $raw_flag && '0' !== $raw_flag;

		$this->simplyin_session->set( 'simplyin_create_account', $create_account ? 'on' : '' );

		wp_send_json_success( [ 'create_account' => $create_account ] );
	}

	public function select_address_ajax_action(): void {

		$logger = simplyin()->get_simplyin_logger();

		if ( check_ajax_referer(
			'simplyin_ajax',
			'ajax_token',
			false
		) ) {

			if ( isset( $_POST['invoice_address'] ) ) {
				$id_invoice_address = sanitize_text_field( wp_unslash( $_POST['invoice_address'] ) );
			} else {
				$this->output_response_bad_request();
			}

			if ( isset( $_POST['delivery_address'] ) ) {
				$id_delivery_address = sanitize_text_field( wp_unslash( $_POST['delivery_address'] ) );
			} else {
				$this->output_response_bad_request();
			}

			if ( isset( $_POST['selected_point'] ) ) {
				$id_selected_point = sanitize_text_field( wp_unslash( $_POST['selected_point'] ) );
			} else {
				$this->output_response_bad_request();
			}

			if ( isset( $_POST['service_type'] ) ) {
				$service_type = sanitize_text_field( wp_unslash( $_POST['service_type'] ) );
			} else {
				$this->output_response_bad_request();
			}
		} else {
			$this->output_response_unauthorized();
		}

		$need_delivery_address = 'address' === $service_type;
		$need_selected_point   = 'parcelLocker' === $service_type;

		$response = new Select_Address_Response();

		if ( $need_delivery_address && '' === $id_delivery_address ) {
			$response->set_is_success( false );
			$response->set_error( 'DELIVERY_ADDRESS_REQUIRED' );
			$this->output_response( $response );
		}

		if ( $need_selected_point && '' === $id_selected_point ) {
			$response->set_is_success( false );
			$response->set_error( 'PARCEL_LOCKER_REQUIRED' );
			$this->output_response( $response );
		}

		if ( '' === $id_delivery_address ) {
			$id_delivery_address = $id_invoice_address;
		}

		$commerce_payload = $this->user_data_commerce_ajax_action();
		$payload          = $this->apply_delivery_selection_to_checkout(
			$commerce_payload,
			$id_invoice_address,
			$id_delivery_address,
			$id_selected_point,
			$service_type
		);

		if ( [] === $payload ) {
			$response->set_is_success( false );
			$response->set_error( 'INVALID_ADDRESS_SELECTION' );
			$this->output_response( $response );

			return;
		}

		$response->set_is_success( true );
		$response->set_warnings( $this->warnings );
		$response->set_redirect( false );
		$response->set_data(
			[
				'invoice_address'  => $payload['invoice_address'] ?? null,
				'delivery_address' => $payload['delivery_address'] ?? null,
				'parcel_address'   => $payload['parcel_address'] ?? null,
				'service_type'     => $payload['service_type'] ?? '',
				'phone_number'     => $payload['phone_number'] ?? null,
			]
		);

		$logger->log_if_wpdebug_on(
			'[select_address] OUTPUT ' . $this->print_r(
				$response->to_array(),
				true
			),
			'widget_ajax'
		);
		$this->output_response( $response );
	}

	public function user_data_commerce_ajax_action(): array {

		$auth_token     = $this->getAuthToken();
		$merchant_token = get_option( 'simplyin_api_key' );

		$cached_data     = $this->simplyin_session->get( 'simplyin_user_data_commerce' );
		$cache_timestamp = $this->simplyin_session->get( 'simplyin_user_data_commerce_timestamp' );

		if ( null !== $cached_data && null !== $cache_timestamp ) {
			if ( ( time() - $cache_timestamp ) < 120 ) {
				return $cached_data;
			}
		}

		$api  = simplyin()->get_api();
		$raw  = $api->sendRequest(
			'userDataCommerce',
			'GET',
			[],
			[
				'apiKey'    => $merchant_token,
				'api_token' => $auth_token,
			]
		);
		$data = json_decode( json_encode( $raw ), true );

		$this->simplyin_session->set( 'simplyin_user_data_commerce', $data );
		$this->simplyin_session->set(
			'simplyin_user_data_commerce_timestamp',
			time()
		);

		return $data;
	}

	private function api_find_address(
		array $payload,
		string $id,
		bool $withMeta = false
	): ?array {
		$data = $payload['data'] ?? $payload['userData'] ?? [];

		foreach (
			[
				'billingAddresses',
				'shippingAddresses',
				'parcelLockers',
			] as $section
		) {
			foreach ( $data[ $section ] ?? [] as $row ) {
				if ( ( $row['_id'] ?? '' ) === $id ) {
					return $withMeta
						? [
							'address'      => $row,
							'address_type' => $section,
						]
						: $row;
				}
			}
		}

		return null;
	}

	private function api_find_phone_number(
		array $payload
	): ?string {
		$data = $payload['data'] ?? $payload['userData'] ?? [];

		if ( ! empty( $data ) && isset( $data['phoneNumber'] ) ) {
			return $data['phoneNumber'];
		}

		return null;
	}

	public function addWarning( $msg ) {
		$this->warnings[] = $msg;
	}

	public function loadCountries() {
		$countries_json_path = simplyin()->get_plugin_dir() . 'public/widget/json/countries.json';
		if ( ! function_exists( 'WP_Filesystem' ) ) {
			require_once ABSPATH . 'wp-admin/includes/file.php';
		}
		$initialized = WP_Filesystem();
		global $wp_filesystem;
		if ( $initialized && $wp_filesystem ) {
			$contents = (string) $wp_filesystem->get_contents( $countries_json_path );
		} elseif ( is_readable( $countries_json_path ) ) {
			$contents = (string) file_get_contents( $countries_json_path );
		}

		$countries = json_decode( $contents, true );

		return is_array( $countries ) ? $countries : [];
	}

	public function get_new_account_form_ajax_action() {
		$current_url = isset( $_POST['url'] ) ? esc_url_raw( wp_unslash( $_POST['url'] ) ) : '';
		$ajax_token  = isset( $_POST['ajax_token'] ) ? sanitize_text_field( wp_unslash( $_POST['ajax_token'] ) ) : '';

		$site_url = get_site_url();
		$home_url = home_url();

		$nonce_result = check_ajax_referer(
			'simplyin_ajax',
			'ajax_token',
			false
		);

		if ( ! $nonce_result ) {
			simplyin()->get_simplyin_logger()->force_log_to_woocommerce(
				sprintf(
					'[Widget_Controller] [get_new_account_form_ajax_action] UNAUTHORIZED nonce failed %s',
					$this->print_r(
						[
							'user'              => is_user_logged_in() ? get_current_user_id() : 0,
							'url'               => $current_url,
							'site_url'          => $site_url,
							'home_url'          => $home_url,
							'option_siteurl'    => get_option( 'siteurl' ),
							'option_home'       => get_option( 'home' ),
							'ajax_token_len'    => strlen( $ajax_token ),
							'ajax_token_hash'   => '' !== $ajax_token ? substr( sha1( $ajax_token ), 0, 12 ) : '',
							'wp_verify_nonce'   => '' !== $ajax_token ? wp_verify_nonce( $ajax_token, 'simplyin_ajax' ) : false,
							'http_host'         => isset( $_SERVER['HTTP_HOST'] ) ? esc_url_raw( wp_unslash( $_SERVER['HTTP_HOST'] ) ) : '',
							'http_referer'      => isset( $_SERVER['HTTP_REFERER'] ) ? esc_url_raw( wp_unslash( $_SERVER['HTTP_REFERER'] ) ) : '',
							'request_uri'       => isset( $_SERVER['REQUEST_URI'] ) ? esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '',
							'http_origin'       => isset( $_SERVER['HTTP_ORIGIN'] ) ? esc_url_raw( wp_unslash( $_SERVER['HTTP_ORIGIN'] ) ) : '',
							'current_url_empty' => '' === $current_url,
						],
						true
					)
				),
				'widget_ajax'
			);

			$this->output_response_unauthorized();
			return;
		}

		simplyin()->get_simplyin_logger()->log_if_wpdebug_on(
			sprintf(
				'[Widget_Controller] [get_new_account_form_ajax_action] INPUT %s',
				$this->print_r(
					[
						'user' => is_user_logged_in() ? get_current_user_id() : 0,
						'url'  => $current_url,
					],
					true
				)
			),
			'widget_ajax'
		);

		if ( ! empty( $current_url ) ) {
			$current_host = wp_parse_url( $current_url, PHP_URL_HOST );
			$home_host    = wp_parse_url( $home_url, PHP_URL_HOST );

			if ( ! $current_host || $current_host !== $home_host ) {
				simplyin()->get_simplyin_logger()->force_log_to_woocommerce(
					sprintf(
						'[Widget_Controller] [get_new_account_form_ajax_action] UNAUTHORIZED invalid URL %s',
						$this->print_r(
							[
								'site_url'         => $site_url,
								'home_url'         => $home_url,
								'current_url'      => $current_url,
								'current_host'     => $current_host,
								'home_host'        => $home_host,
								'option_siteurl'   => get_option( 'siteurl' ),
								'option_home'      => get_option( 'home' ),
								'request_uri'      => isset( $_SERVER['REQUEST_URI'] ) ? esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '',
								'http_origin'      => isset( $_SERVER['HTTP_ORIGIN'] ) ? esc_url_raw( wp_unslash( $_SERVER['HTTP_ORIGIN'] ) ) : '',
							],
							true
						)
					),
					'widget_ajax'
				);

				$this->output_response_unauthorized();
				return;
			}

			$parsed_url = wp_parse_url( $current_url );
			$path       = $parsed_url['path'] ?? '';

			// Check if this is a payment retry view by checking the pay_for_order query parameter
			$query        = isset( $parsed_url['query'] ) ? $parsed_url['query'] : '';
			$query_params = [];
			wp_parse_str( $query, $query_params );

			$is_payment_retry = isset( $query_params['pay_for_order'] ) && 'true' === $query_params['pay_for_order'];

			if ( $is_payment_retry ) {
				simplyin()->get_simplyin_logger()->log_if_wpdebug_on(
					sprintf(
						'[Widget_Controller] [get_new_account_form_ajax_action] User is on payment retry page (pay_for_order=true), blocking form display. URL: %s',
						$current_url
					),
					'widget_ajax'
				);

				$response = new New_Account_Form_Response( '', false );
				$this->output_response( $response );
				return;
			}

			$block_endpoints = [
				get_option( 'woocommerce_checkout_order_received_endpoint', 'order-received' ),
				get_option( 'woocommerce_checkout_order_pay_endpoint', 'order-pay' ),
			];

			if ( ! empty( $path ) ) {
				foreach ( $block_endpoints as $endpoint ) {
					if ( empty( $endpoint ) ) {
						continue;
					}

					$endpoint = sanitize_title( $endpoint );

					if ( strpos( $path, '/' . $endpoint . '/' ) !== false ) {
						simplyin()->get_simplyin_logger()->log_if_wpdebug_on(
							sprintf(
								'[Widget_Controller] [get_new_account_form_ajax_action] User is on blocked page (%s), blocking form display. URL: %s',
								$endpoint,
								$current_url
							),
							'widget_ajax'
						);

						$response = new New_Account_Form_Response( '', false );
						$this->output_response( $response );
						return;
					}
				}
			}
		}

		$countries = $this->loadCountries();

		// Regulatory consent links, sourced from the backend-synced docs cache with a
		// canonical hardcoded fallback. Links are canonical (no /en/ vs pl branching —
		// the simply.in site self-localizes). Terms is ALWAYS the B2C document, never B2B.
		$docs_array = get_option( 'SimplyInDocs' );
		if ( ! is_array( $docs_array ) ) {
			$docs_array = [];
		}

		$terms_link   = esc_url(
			$this->resolve_synced_doc_url( 'tc-b2c-pl', $docs_array, 'https://simply.in/terms-b2c/' )
		);
		$privacy_link = esc_url(
			$this->resolve_synced_doc_url( 'gdpr-pl', $docs_array, 'https://simply.in/privacy-policy/' )
		);

		$prefix  = '+48';
		$checked = false;
		$phone   = '';

		if ( is_user_logged_in() ) {
			$raw_phone = get_user_meta(
				get_current_user_id(),
				'billing_phone',
				true
			);
			if ( $raw_phone && preg_match(
					'/^\+(\d+)\s+(.+)$/',
					$raw_phone,
					$m
				) ) {
				$prefix  = '+' . $m[1];
				$phone   = $m[2];
				$checked = true;
			}
		}

		$module_path = plugin_dir_url(
			dirname(
				__DIR__,
				3
			) . '/simplyin.php'
		);

		$html = $this->get_html(
			'create_account.php',
			[
				'module_path'     => $module_path,
				'countries'       => $countries,
				'terms_link'      => $terms_link,
				'privacy_link'    => $privacy_link,
				'selected_prefix' => $prefix,
				'phone'           => $phone,
				'checked'         => $checked,
			]
		);

		simplyin()->get_simplyin_logger()->log_if_wpdebug_on(
			sprintf(
				'[Widget_Controller] [get_new_account_form_ajax_action] OUTPUT %s',
				$this->print_r(
					[
						'prefix'   => $prefix,
						'phone'    => $phone,
						'checked'  => $checked,
						'html_len' => strlen( $html ),
					],
					true
				)
			),
			'widget_ajax'
		);

		$response = new New_Account_Form_Response( $html );
		$this->output_response( $response );
	}

	/**
	 * Resolve a regulatory document URL from the backend-synced docs cache.
	 *
	 * Honors whatever the live sync actually writes, in priority order:
	 *   1. per-key option written by Simplyin_Sync (get_option("SimplyInDoc_<key>")),
	 *   2. the array option written by SimplyIn_Background_Process (get_option("SimplyInDocs")),
	 *   3. the canonical hardcoded fallback.
	 *
	 * @param string               $key        Doc key, e.g. 'tc-b2c-pl' or 'gdpr-pl'.
	 * @param array<string,string> $docs_array Value of the 'SimplyInDocs' array option.
	 * @param string               $fallback   Canonical fallback URL.
	 */
	private function resolve_synced_doc_url( string $key, array $docs_array, string $fallback ): string {
		$candidates = array(
			get_option( 'SimplyInDoc_' . $key ),
			isset( $docs_array[ $key ] ) ? $docs_array[ $key ] : null,
		);

		foreach ( $candidates as $candidate ) {
			if ( ! is_string( $candidate ) ) {
				continue;
			}
			$candidate = trim( $candidate );
			// Accept a synced value only if it survives URL sanitization; otherwise fall
			// through to the next source and finally the canonical fallback, so the consent
			// link never renders with an empty href.
			if ( '' !== $candidate && '' !== esc_url_raw( $candidate ) ) {
				return $candidate;
			}
		}

		return $fallback;
	}

	/**
	 * AJAX handler that explicitly logs the user out from Simply.IN checkout – it clears
	 * all related tokens stored in the PHP session.
	 *
	 * JS can call it via:  action: 'simplyin_logout'.
	 */
	public function logout_ajax_action(): void {
		if ( ! check_ajax_referer( 'simplyin_ajax', 'ajax_token', false ) ) {
			$this->output_response_unauthorized();
			return;
		}

		$this->reset_simplyin_session();
		wp_send_json_success();
	}

	private function reset_simplyin_session(): void {
		$previous_token      = (string) $this->simplyin_session->get( 'simplyin_auth_token', '' );
		$previous_session_id = (string) $this->simplyin_session->get( 'id', '' );
		$this->simplyin_session->clear();
		Payment_Fields::clear_methods_cache( $previous_token, $previous_session_id );

		$this->simplyin_session->set( 'simplyin_user_data_commerce', null );
		$this->simplyin_session->set(
			'simplyin_user_data_commerce_timestamp',
			null
		);
	}

	private function get_customer_message_on_unauthorized(): string {
		return '';
	}

	/**
	 * Single source of truth for the `language` value the backend locale validator accepts:
	 * an uppercase, 2-letter code (e.g. "PL", "EN").
	 */
	private function get_shop_language(): string {
		return strtoupper( substr( get_locale(), 0, 2 ) );
	}

	private function update_array_with_plugin_info( array $body_data ): array {
		$body_data['pluginVersion'] = SimplyInGetPluginVersion();
		$body_data['shopName']      = addslashes( sanitize_text_field( get_bloginfo( 'name' ) ) );
		$body_data['platform']      = 'WooCommerce';
		$body_data['shopVersion']   = defined( 'WC_VERSION' ) ? WC_VERSION : '';
		$body_data['language']      = $this->get_shop_language();

		return $body_data;
	}

	/**
	 * @param Check_Code_Response|Check_Push_Notification_Response $response  Login or session response object.
	 * @param array<string, mixed>                                 $user_data Normalized commerce data root for fallbacks.
	 * @param array<string, mixed>                                 $sync      Payload from sync_checkout_delivery_from_commerce_payload().
	 */
	private function apply_invoice_and_phone_to_login_response(
		$response,
		array $user_data,
		array $sync
	): void {
		if ( [] !== $sync ) {
			$response->set_checkout_sync_payload( $sync );
			if ( ! empty( $sync['invoice_address'] ) && is_array( $sync['invoice_address'] ) ) {
				$response->set_invoice_address( $sync['invoice_address'] );
			}
			if ( isset( $sync['phone_number'] ) && is_string( $sync['phone_number'] ) ) {
				$response->set_phone_number( $sync['phone_number'] );
			}

			return;
		}

		if ( isset( $user_data['phoneNumber'] ) ) {
			$response->set_phone_number( (string) $user_data['phoneNumber'] );
		}

		if ( ! empty( $user_data['billingAddresses'] ) ) {
			$response->set_invoice_address( $user_data['billingAddresses'][0] );
		}
	}

	private function wc_checkout_customer_available(): bool {
		return function_exists( 'WC' ) && WC()->customer;
	}

	private function ensure_wc_session_loaded_for_ajax(): void {
		if ( ! defined( 'DOING_AJAX' ) || ! DOING_AJAX ) {
			return;
		}
		if ( ! function_exists( 'WC' ) ) {
			return;
		}
		if ( function_exists( 'wc_load_cart' ) ) {
			wc_load_cart();
		}
	}

	private function refresh_shipping_mapping_cookies_if_cart(): void {
		if ( ! function_exists( 'WC' ) || ! WC()->cart || WC()->cart->is_empty() ) {
			return;
		}
		( new Mapping() )->handle_checkout();
	}

	private function is_point_delivery_service_type( string $service_type ): bool {
		return 'parcel' === $service_type || 'delivery' === $service_type;
	}

	private function clear_simplyin_parcel_locker_extras(): void {
		$this->simplyin_session->set( 'simplyin_parcel_locker_id', '' );
		$this->simplyin_session->set( 'simplyin_parcel_provider', '' );
		$this->simplyin_session->set( 'simplyin_parcel_locker_full', null );
	}

	private function persist_parcel_locker_row_to_session( array $commerce_payload, string $point_id ): void {
		if ( '' === $point_id ) {
			return;
		}

		$parcel_address = $this->api_find_address( $commerce_payload, $point_id );
		if ( ! is_array( $parcel_address ) ) {
			return;
		}

		$locker_id = isset( $parcel_address['lockerId'] ) ? (string) $parcel_address['lockerId'] : '';
		$provider  = isset( $parcel_address['providerName'] ) ? (string) $parcel_address['providerName'] : '';

		if ( '' !== $locker_id ) {
			$this->simplyin_session->set( 'simplyin_parcel_locker_id', $locker_id );
		}
		if ( '' !== $provider ) {
			$this->simplyin_session->set( 'simplyin_parcel_provider', $provider );
		}

		$this->simplyin_session->set( 'simplyin_parcel_locker_full', $parcel_address );
	}

	private function wc_apply_address( array $addr, string $scope ): void {
		$customer = WC()->customer;
		$prefix   = $scope . '_';

		$customer->{"set_{$prefix}first_name"}( $addr['name'] ?? '' );
		$customer->{"set_{$prefix}last_name"}( $addr['surname'] ?? '' );
		$customer->{"set_{$prefix}company"}( $addr['companyName'] ?? '' );
		$customer->{"set_{$prefix}address_1"}( $addr['street'] ?? '' );
		$customer->{"set_{$prefix}address_2"}( $addr['appartmentNumber'] ?? '' );
		$customer->{"set_{$prefix}city"}( $addr['city'] ?? '' );
		$customer->{"set_{$prefix}postcode"}( $addr['postalCode'] ?? '' );
		$customer->{"set_{$prefix}country"}( $addr['country'] ?? '' );
		$customer->save();
	}

	/**
	 * Persist Simply.IN delivery choice to session + WC_Customer; build payload for insertFormData.
	 *
	 * @return array<string, mixed> Empty if WC customer missing or invoice id unknown in commerce payload.
	 */
	private function apply_delivery_selection_to_checkout(
		array $commerce_payload,
		string $invoice_id,
		string $delivery_id,
		string $point_id,
		string $service_type
	): array {
		$this->ensure_wc_session_loaded_for_ajax();

		if ( ! $this->wc_checkout_customer_available() ) {
			return [];
		}

		if ( '' === $invoice_id ) {
			return [];
		}

		$invoice_row = $this->api_find_address( $commerce_payload, $invoice_id );
		if ( ! is_array( $invoice_row ) ) {
			return [];
		}

		if ( '' === $delivery_id ) {
			$delivery_id = $invoice_id;
		}

		$this->simplyin_session->set( 'simplyin_billing', $invoice_id );
		$this->simplyin_session->set( 'simplyin_shipping', $delivery_id );
		$this->simplyin_session->set( 'simplyin_parcel', $point_id );
		$this->simplyin_session->set( 'service_type', $service_type );

		$is_point = $this->is_point_delivery_service_type( $service_type );
		if ( $is_point ) {
			if ( '' === $point_id ) {
				$this->clear_simplyin_parcel_locker_extras();
			} else {
				$this->persist_parcel_locker_row_to_session( $commerce_payload, $point_id );
			}
		} else {
			$this->clear_simplyin_parcel_locker_extras();
		}

		$delivery_row   = $this->api_find_address( $commerce_payload, $delivery_id );
		$parcel_row     = '' !== $point_id ? $this->api_find_address( $commerce_payload, $point_id ) : null;
		$phone_number   = $this->api_find_phone_number( $commerce_payload );

		$this->wc_apply_address( $invoice_row, 'billing' );
		if ( is_array( $delivery_row ) ) {
			$this->wc_apply_address( $delivery_row, 'shipping' );
		}

		$parcel_for_response = $parcel_row;
		if ( null === $parcel_for_response && $is_point ) {
			$cached              = $this->simplyin_session->get( 'simplyin_parcel_locker_full', null );
			$parcel_for_response = is_array( $cached ) ? $cached : null;
		}

		$response_delivery = $delivery_row;
		if ( $is_point ) {
			$response_delivery = null;
		}

		$result = [
			'invoice_address'  => $invoice_row,
			'delivery_address' => $response_delivery,
			'parcel_address'   => $parcel_for_response,
			'service_type'     => $service_type,
			'phone_number'     => $phone_number,
		];

		$this->refresh_shipping_mapping_cookies_if_cart();

		return $result;
	}

	private function pick_default_parcel_from_commerce_display( array $commerce_payload ): array {
		$list_payload = $commerce_payload;
		if ( ! isset( $list_payload['data'] ) && isset( $list_payload['userData'] ) ) {
			$list_payload = [ 'data' => $list_payload['userData'] ];
		}
		if ( ! isset( $list_payload['data'] ) || ! is_array( $list_payload['data'] ) ) {
			return [
				'point_id'         => '',
				'api_service_type' => '',
			];
		}

		$lockers = $this->getParcelLockers( $list_payload, false );
		foreach ( $lockers as $locker ) {
			if ( empty( $locker['selected'] ) ) {
				continue;
			}

			return [
				'point_id'         => (string) ( $locker['_id'] ?? '' ),
				'api_service_type' => (string) ( $locker['service_type'] ?? '' ),
			];
		}

		return [
			'point_id'         => '',
			'api_service_type' => '',
		];
	}

	/**
	 * @return array{invoice_id: string, delivery_id: string, point_id: string, service_type: string}|null
	 */
	private function resolve_stored_or_default_delivery_ids( array $commerce_payload ): ?array {
		$stored_billing = (string) $this->simplyin_session->get( 'simplyin_billing', '' );
		if ( '' !== $stored_billing ) {
			$shipping     = (string) $this->simplyin_session->get( 'simplyin_shipping', '' );
			$point_id     = (string) $this->simplyin_session->get( 'simplyin_parcel', '' );
			$service_type = (string) $this->simplyin_session->get( 'service_type', '' );

			if ( $this->is_point_delivery_service_type( $service_type ) && '' === $point_id ) {
				$hint = $this->pick_default_parcel_from_commerce_display( $commerce_payload );
				if ( '' !== $hint['point_id'] ) {
					$point_id = $hint['point_id'];
				}
			}

			return [
				'invoice_id'   => $stored_billing,
				'delivery_id'  => '' !== $shipping ? $shipping : $stored_billing,
				'point_id'     => $point_id,
				'service_type' => $service_type,
			];
		}

		$data    = $commerce_payload['data'] ?? $commerce_payload['userData'] ?? [];
		$billing = $data['billingAddresses'] ?? [];
		if ( ! is_array( $billing ) || [] === $billing ) {
			return null;
		}

		$first   = $billing[0];
		$invoice = is_array( $first ) ? (string) ( $first['_id'] ?? '' ) : '';
		if ( '' === $invoice ) {
			return null;
		}

		$base = [
			'invoice_id'   => $invoice,
			'delivery_id'  => $invoice,
			'point_id'     => '',
			'service_type' => '',
		];

		$hint = $this->pick_default_parcel_from_commerce_display( $commerce_payload );
		if ( '' !== $hint['point_id'] ) {
			$base['point_id']     = $hint['point_id'];
			$base['service_type'] = ( 'parcel_machine' === $hint['api_service_type'] ) ? 'parcel' : 'delivery';
		}

		return $base;
	}

	/**
	 * After login or on checkout load: restore saved selection or default courier + billing address.
	 *
	 * @return array<string, mixed> Empty when nothing applied.
	 */
	private function sync_checkout_delivery_from_commerce_payload( array $commerce_payload ): array {
		$ids = $this->resolve_stored_or_default_delivery_ids( $commerce_payload );
		if ( null === $ids ) {
			return [];
		}

		return $this->apply_delivery_selection_to_checkout(
			$commerce_payload,
			$ids['invoice_id'],
			$ids['delivery_id'],
			$ids['point_id'],
			$ids['service_type']
		);
	}

	public function maybe_resync_checkout_from_simplyin_session(): void {
		if ( ! function_exists( 'is_checkout' ) || ! is_checkout() ) {
			return;
		}
		if ( function_exists( 'is_wc_endpoint_url' ) && is_wc_endpoint_url( 'order-pay' ) ) {
			return;
		}
		if ( ( defined( 'DOING_AJAX' ) && DOING_AJAX ) || ( defined( 'REST_REQUEST' ) && REST_REQUEST ) ) {
			return;
		}
		if ( ! function_exists( 'WC' ) ) {
			return;
		}

		$auth = $this->getAuthToken();
		if ( ! is_string( $auth ) || '' === $auth ) {
			return;
		}

		if ( function_exists( 'wc_load_cart' ) ) {
			wc_load_cart();
		}

		if ( ! $this->wc_checkout_customer_available() ) {
			return;
		}

		try {
			$commerce_payload = $this->user_data_commerce_ajax_action();
		} catch ( \Throwable $e ) {
			return;
		}

		if ( ! is_array( $commerce_payload ) || ! empty( $commerce_payload['code'] ) ) {
			return;
		}

		$this->sync_checkout_delivery_from_commerce_payload( $commerce_payload );
	}
}
