<?php
/**
 * ShipStation REST API Orders Controller file.
 *
 * @package WeDevs\DokanPro\Modules\ShipStation
 */

namespace WeDevs\DokanPro\Modules\ShipStation\REST;

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

use WC_Order;
use WC_Order_Item;
use WC_Order_Item_Coupon;
use WC_Order_Item_Product;
use WC_Order_Item_Tax;
use WC_Tax;
use WP_REST_Request;
use WP_REST_Response;
use WP_REST_Server;
use WC_Logger;
use WP_REST_Controller;
use Automattic\WooCommerce\Utilities\NumberUtil;

/**
 * VendorOrdersController class.
 */
class VendorOrdersController extends WP_REST_Controller {

	/**
	 * Vendor ID resolved from the authenticated WC API user.
	 *
	 * @var int
	 */
	public int $vendor_id = 0;

	/**
	 * Export statuses.
	 *
	 * @var array
	 */
	public array $export_statuses = array();

	/**
	 * Flag for logging feature.
	 * `true` means log feature is on.
	 *
	 * @var boolean
	 */
	public static $logging_enabled = null;

	/**
	 * Shipment status.
	 *
	 * @var string
	 */
	public ?string $shipped_status = null;

	/**
	 * Gift enable flag.
	 *
	 * @var boolean
	 */
	public static $gift_enabled = false;

	/**
	 * Status mapping.
	 *
	 * @var array
	 */
	public array $status_mapping = array();

	/**
	 * ShipStation status for awaiting payment.
	 *
	 * @var string
	 */
	public const AWAITING_PAYMENT_STATUS = 'AwaitingPayment';

	/**
	 * ShipStation status for awaiting shipment.
	 *
	 * @var string
	 */
	public const AWAITING_SHIPMENT_STATUS = 'AwaitingShipment';

	/**
	 * ShipStation status for on-hold.
	 *
	 * @var string
	 */
	public const ON_HOLD_STATUS = 'OnHold';

	/**
	 * ShipStation status for completed.
	 *
	 * @var string
	 */
	public const COMPLETED_STATUS = 'Completed';

	/**
	 * ShipStation status for Cancelled.
	 *
	 * @var string
	 */
	public const CANCELLED_STATUS = 'Cancelled';

	/**
	 * ShipStation status for Payment Cancelled.
	 *
	 * @var string
	 */
	public const PAYMENT_CANCELLED_STATUS = 'PaymentCancelled';

	/**
	 * ShipStation status for Payment Failed.
	 *
	 * @var string
	 */
	public const PAYMENT_FAILED_STATUS = 'PaymentFailed';

	/**
	 * ShipStation status for Paid.
	 *
	 * @var string
	 */
	public const PAID_STATUS = 'Paid';

	/**
	 * Order meta keys.
	 *
	 * @var array
	 */
	public static array $order_meta_keys = array(
		'is_gift'      => 'shipstation_is_gift',
		'gift_message' => 'shipstation_gift_message',
	);

	/**
	 * WooCommerce status prefix.
	 *
	 * @var string
	 */
	public static $wc_status_prefix = 'wc-';

	/**
	 * Stores logger class.
	 *
	 * @var WC_Logger
	 */
	private static $log = null;

	/**
	 * Log a message using WC_Logger.
	 *
	 * @param string $message Message to log.
	 *
	 * @return void
	 */
	public static function log( $message ) {
		if ( is_null( self::$logging_enabled ) ) {
			self::$logging_enabled = dokan_get_option( 'enable_shipstation_logging', 'dokan_general', 'off' );
		}

		if ( 'off' === self::$logging_enabled ) {
			return;
		}

		if ( is_null( self::$log ) ) {
			self::$log = new WC_Logger();
		}

		self::$log->add( 'dokan-shipstation', $message );
	}


	/**
	 * Per-order currency code used for export context.
	 *
	 * @var string
	 */
	private string $currency_code = '';

	/**
	 * Per-order exchange rate used to convert monetary values for ShipStation.
	 *
	 * @var float
	 */
	private float $exchange_rate = 1.00;

	/**
	 * Whether to export discounts as a separate line item.
	 *
	 * @var bool
	 */
	private bool $export_discounts_as_separate_item = true;

	/**
	 * Initialize per-order context values using filters.
	 *
	 * @param WC_Order $order Order object.
	 * @return void
	 */
	private function set_order_context( WC_Order $order ): void {
		// Reset per-order context to defaults, in case it was previously set.
		$this->currency_code                     = '';
		$this->exchange_rate                     = 1.00;
		$this->export_discounts_as_separate_item = true;

		/**
		 * Currency code can be filtered by 3rd parties.
		 *
		 * @param string   $currency_code Currency code.
		 * @param WC_Order $order         Order object.
		 *
		 * @since 5.0.0
		 */
		$this->currency_code = apply_filters( 'dokan_shipstation_export_currency_code', $order->get_currency(), $order );
		/**
		 * Exchange rate can be filtered by 3rd parties.
		 *
		 * @param float    $exchange_rate Exchange rate.
		 * @param WC_Order $order         Order object.
		 *
		 * @since 5.0.0
		 */
		$this->exchange_rate = (float) apply_filters( 'dokan_shipstation_export_exchange_rate', 1.00, $order );
		/**
		 * Discount export behavior can be filtered by 3rd parties.
		 *
		 * @param bool     $export_discounts_as_separate_item Whether to export discounts as a separate line item.
		 * @param WC_Order $order                             Order object.
		 *
		 * @since 5.0.0
		 */
		$this->export_discounts_as_separate_item = (bool) apply_filters( 'dokan_shipstation_export_discounts_as_separate_item', true, $order );
	}

	/**
	 * Get the per-order currency code.
	 */
	private function get_currency_code(): string {
		return $this->currency_code;
	}

	/**
	 * Get the per-order exchange rate.
	 */
	private function get_exchange_rate(): float {
		return $this->exchange_rate;
	}

	/**
	 * Should export discounts as a separate line item.
	 */
	private function should_export_discounts_as_separate_item(): bool {
		return $this->export_discounts_as_separate_item;
	}

	public function __construct() {
		$this->rest_base = 'orders';
		$this->namespace = 'wc-shipstation/v1';
		$this->status_mapping = array(
			self::AWAITING_PAYMENT_STATUS  => array( 'wc-pending' ),
			self::AWAITING_SHIPMENT_STATUS => array( 'wc-processing' ),
			self::ON_HOLD_STATUS           => array( 'wc-on-hold' ),
			self::COMPLETED_STATUS         => array( 'wc-completed' ),
			self::CANCELLED_STATUS         => array( 'wc-cancelled' ),
		);
	}

	/**
	 * Resolve vendor context lazily.
	 *
	 * REST authentication hasn't occurred when constructors run,
	 * so get_current_user_id() must be called lazily in request handlers.
	 * This method resolves the vendor ID and loads vendor-specific settings on first access.
	 *
	 * @return void
	 */
	private function resolve_vendor_context(): void {
		if ( 0 !== $this->vendor_id ) {
			return;
		}

		$this->vendor_id = get_current_user_id();

		$vendor_settings       = \WeDevs\DokanPro\Modules\ShipStation\VendorOrderStatusSettings::get( $this->vendor_id );
		$vendor_export         = ! empty( $vendor_settings['export_statuses'] ) && is_array( $vendor_settings['export_statuses'] )
			? $vendor_settings['export_statuses']
			: array();

		$this->export_statuses = $vendor_export;
		$this->shipped_status  = $vendor_settings['shipped_status'];
	}

	/**
	 * Get the authenticated vendor ID.
	 *
	 * @return int
	 */
	private function get_vendor_id(): int {
		$this->resolve_vendor_context();

		return $this->vendor_id;
	}

	/**
	 * Get the vendor's export statuses.
	 *
	 * @return array
	 */
	private function get_export_statuses(): array {
		$this->resolve_vendor_context();

		return $this->export_statuses;
	}

	/**
	 * Get the vendor's shipped status.
	 *
	 * @return string|null
	 */
	private function get_shipped_status(): ?string {
		$this->resolve_vendor_context();

		return $this->shipped_status;
	}

	/**
	 * Register the routes for the controller.
	 */
	public function register_routes(): void {
		// Register the endpoint for retrieving order data.
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base,
			array(
				'methods'             => WP_REST_Server::READABLE,
				'callback'            => array( $this, 'get_orders' ),
				'permission_callback' => array( $this, 'check_get_permission' ),
				'args'                => array(
					'modified_after' => array(
						'description'       => __( 'Return orders modified after this date/time (ISO8601, UTC). Leave empty to ignore.', 'dokan' ),
						'type'              => 'string',
						'validate_callback' => function ( $param ) {
							if ( empty( $param ) ) {
								return true;
							}
							return false !== strtotime( $param );
						},
					),
					'page'           => array(
						'description'       => __( 'Page number of the results to return.', 'dokan' ),
						'type'              => 'integer',
						'default'           => 1,
						'sanitize_callback' => function ( $value ) {
							return max( 1, absint( $value ) );
						},
						'validate_callback' => function ( $param ) {
							return is_numeric( $param );
						},
					),
					'per_page'       => array(
						'description'       => __( 'Maximum number of items to return per page (1–500).', 'dokan' ),
						'type'              => 'integer',
						'default'           => 100,
						'sanitize_callback' => function ( $value ) {
							return min( max( 1, absint( $value ) ), 500 ); // Limit between 1 and 500.
						},
						'validate_callback' => function ( $param ) {
							return is_numeric( $param );
						},
					),
					'status_mapping' => array(
						'description'       => __( 'Mapping of WooCommerce order statuses to ShipStation statuses. Format: wc_status:ShipStationStatus (e.g., processing:AwaitingShipment). Accepts multiple values.', 'dokan' ),
						'type'              => 'array',
						'sanitize_callback' => function ( $value ) {
							if ( empty( $value ) ) {
								return array();
							}
							return is_array( $value ) ? $value : array( $value );
						},
					),
				),
			)
		);

		// Register the endpoint for updating order shipment data.
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/shipments',
			array(
				'methods'             => WP_REST_Server::CREATABLE,
				'callback'            => array( $this, 'update_orders_shipments' ),
				'permission_callback' => array( $this, 'check_update_permission' ),
			)
		);
	}

	/**
	 * REST API permission callback.
	 *
	 * @return bool|WP_Error
	 */
	public function check_get_permission() {
		if ( current_user_can( 'manage_options' ) || current_user_can( 'dokandar' ) ) {
			return true;
		}

		return new \WP_Error(
			'dokan_pro_permission_failure',
			__( 'Sorry! You are not permitted to do current action.', 'dokan' ),
			[ 'status' => 403 ]
		);
	}

	/**
	 * REST API permission callback.
	 *
	 * @return bool|WP_Error
	 */
	public function check_update_permission() {
		return $this->check_get_permission();
	}

	/**
	 * Get mapped WC order status with ShipStation status.
	 *
	 * @return array
	 */
	public function get_order_status_mapping(): array {
		$mapped_status = array();
		foreach ( $this->status_mapping as $shipstation_status => $wc_statuses ) {
			if ( ! is_array( $wc_statuses ) ) {
				continue;
			}

			$mapped_status[ $shipstation_status ] = array_map(
				function ( $status ) {
					return str_replace( self::$wc_status_prefix, '', $status );
				},
				$wc_statuses
			);
		}

		return $mapped_status;
	}

	/**
	 * Get the shipstation status from WC order status.
	 *
	 * @param string $order_status The order status to map.
	 *
	 * @return string
	 */
	public function get_shipstation_status_from_order( string $order_status ): string {
		$status_mapping = $this->get_order_status_mapping();

		foreach ( $status_mapping as $shipstation_status => $wc_statuses ) {
			if ( in_array( $order_status, $wc_statuses, true ) ) {
				return $shipstation_status;
			}
		}

		return 'Unknown';
	}

	/**
	 * Get the order status mapping.
	 *
	 * @param string $status The ShipStation status to map.
	 *
	 * @return array
	 */
	public function get_order_status_from_shipstation( string $status ): array {
		$status_mapping = $this->get_order_status_mapping();

		foreach ( $status_mapping as $shipstation_status => $wc_statuses ) {
			if ( $status === $shipstation_status ) {
				return $wc_statuses;
			}
		}

		return array();
	}

	/**
	 * Retrieve the orders data.
	 *
	 * @param WP_REST_Request $request Request object.
	 *
	 * @return WP_REST_Response
	 */
	public function get_orders( WP_REST_Request $request ): WP_REST_Response {
		$request_params = $request->get_params();

		/**
		* Action hook before the `/orders` REST API endpoint process the request parameter.
		*
		* @param array $request_params
		*
		* @since 5.0.0
		*/
		do_action( 'dokan_rest_shipstation_get_orders_before_process_request', $request_params );

		// Get parameters.
		$modified_after = isset( $request_params['modified_after'] ) ? strtotime( $request_params['modified_after'] ) : null;
		$page           = absint( $request_params['page'] ); // Default to page 1.
		$per_page       = intval( $request_params['per_page'] ); // Default to 100 items per page.
		$status_mapping = isset( $request_params['status_mapping'] ) ? $request_params['status_mapping'] : array();

		$status_mapping = is_array( $status_mapping ) ? wc_clean( $status_mapping ) : array( wc_clean( $status_mapping ) );
		$order_statuses = array();

		foreach ( $status_mapping as $status ) {
			$parts = explode( ':', $status );

			if ( ! is_array( $parts ) || count( $parts ) !== 2 || empty( $parts[0] ) || empty( $parts[1] ) ) {
				continue;
			}

			$wc_statuses = $this->get_order_status_from_shipstation( $parts[1] );

			if ( empty( $wc_statuses ) ) {
				continue;
			}

			foreach ( $wc_statuses as $wc_status ) {
				$order_statuses[] = self::$wc_status_prefix . strtolower( $wc_status );
			}
		}

		if ( empty( $order_statuses ) ) {
			// If no statuses are provided, use the default export statuses.
			// This is to ensure that we always have some statuses to query.
			// Default export statuses can be defined in the integration class.
			$order_statuses = $this->get_export_statuses();
			self::log( __( 'No order statuses provided in the request. Using default export statuses from the settings.', 'dokan' ) );
		} else {
			// Only use the order status that has been set from the ShipStation plugin settings.
			$order_statuses = array_unique( $order_statuses );
			$order_statuses = array_intersect(
				$order_statuses,
				$this->get_export_statuses()
			);

			// If no valid order statuses,
			// use unregistered/invalid order statuses to make sure the WC Order query return 0 order.
			if ( empty( $order_statuses ) ) {
				$order_statuses = array( 'wc-shipstation-unknown' );
			}
		}

		$args = array(
			'seller_id' => $this->get_vendor_id(),
			'status'    => $order_statuses,
			'limit'     => $per_page,
			'paged'     => $page,
			'paginate'  => true,
			'return'    => 'ids',
			'orderby'   => 'modified',
			'order'     => 'DESC',
		);

		if ( ! empty( $modified_after ) ) {
			$args['date_modified'] = '>=' . $modified_after;
		}

		$results = dokan()->order->all( $args );

		if ( is_wp_error( $results ) ) {
			return new WP_REST_Response( array( 'message' => __( 'Error retrieving orders.', 'dokan' ) ), 500 );
		}

		$total_orders = $results->total;

		// Calculate pagination information.
		$total_pages = $results->max_num_pages;
		$has_more    = $page < $total_pages;

		// Prepare the response data.
		$sales_orders_data = array(
			'sales_orders' => array(),
			'pagination'   => array(
				'page'        => $page,
				'per_page'    => $per_page,
				'total'       => $total_orders,
				'total_pages' => $total_pages,
				'has_more'    => $has_more,
			),
		);

		if ( empty( $results->orders ) || empty( $total_orders ) ) {
			// No sales orders found, return an empty response.
			return new WP_REST_Response( $sales_orders_data, 200 );
		}

		foreach ( $results->orders as $order_id ) {
			/**
			 * Allow third party to skip the export of certain order ID.
			 *
			 * @param boolean $flag Flag to skip the export.
			 * @param int     $order_id Order ID.
			 *
			 * @since 5.0.0
			 */
			if ( ! apply_filters( 'dokan_shipstation_export_order', true, $order_id ) ) {
				continue;
			}

			/**
			 * Allow third party to change the order object.
			 *
			 * @param WC_Order $order Order object.
			 *
			 * @since 5.0.0
			 */
			$order = apply_filters( 'dokan_shipstation_export_get_order', wc_get_order( $order_id ) );

			if ( ! ( $order instanceof WC_Order ) ) {
				/* translators: 1: order id */
				self::log( sprintf( __( 'Order %s can not be found.', 'dokan' ), $order_id ) );
				continue;
			}

			$sales_orders_data['sales_orders'][] = $this->get_order_data( $order );

			// Add order note to indicate it has been exported to Shipstation.
			if ( 'yes' !== $order->get_meta( '_shipstation_exported', true ) ) {
				$order->add_order_note( __( 'Order has been exported to Shipstation', 'dokan' ) );
				$order->update_meta_data( '_shipstation_exported', 'yes' );
				$order->save_meta_data();
			}
		}

		return new WP_REST_Response( $sales_orders_data, 200 );
	}

	/**
	 * Get order data for the response.
	 *
	 * @param WC_Order $order The order object.
	 *
	 * @return array
	 */
	public function get_order_data( WC_Order $order ): array {
		$extra_args = array();
		// Initialize per-order export context.
		$this->set_order_context( $order );

		$currency_code = $this->get_currency_code();

		$paid_date = ! empty( $order->get_date_paid() ) ? $this->get_shipstation_date_format( $order->get_date_paid()->date( 'Y-m-d' ), $order->get_date_paid()->date( 'H:i:s' ) ) : '';

		$formatted_order_number   = ltrim( $order->get_order_number(), '#' );
		$shipstation_order_status = $this->get_shipstation_status_from_order( $order->get_status() );

		if ( 'Unknown' === $shipstation_order_status ) {
			// translators: 1: order id, 2: WC order status, 3: shipstation order status.
			self::log( sprintf( __( 'Order %1$s has an unmapped WooCommerce status (%2$s). Defaulting to ShipStation status "%3$s". Please review your status mappings.', 'dokan' ), $order->get_id(), $order->get_status(), $shipstation_order_status ) );
		}

		$order_data = array(
			'order_id'               => $order->get_id(),
			'order_number'           => $formatted_order_number,
			'status'                 => $shipstation_order_status,
			'paid_date'              => $paid_date,
			'requested_fulfillments' => $this->get_requested_fulfillments( $order, $extra_args ),
			'buyer'                  => $this->get_buyer( $order ),
			'bill_to'                => array(
				'email'          => $order->get_billing_email(),
				'name'           => $order->get_billing_first_name() . ' ' . $order->get_billing_last_name(),
				'phone'          => $order->get_billing_phone(),
				'company'        => $order->get_billing_company(),
				'address_line_1' => $order->get_billing_address_1(),
				'address_line_2' => $order->get_billing_address_2(),
				'city'           => $order->get_billing_city(),
				'state_province' => $order->get_billing_state(),
				'postal_code'    => $order->get_billing_postcode(),
				'country_code'   => $order->get_billing_country(),
			),
			'currency'               => $currency_code,
			'payment'                => $this->get_payment_info( $order, $extra_args ),
			'ship_from'              => $this->get_vendor_ship_from_address(),
			'order_url'              => $order->get_checkout_order_received_url(),
			'notes'                  => $this->get_notes( $order ),
			'created_date_time'      => $order->get_date_created() ? $this->get_shipstation_date_format( $order->get_date_created()->date( 'Y-m-d' ), $order->get_date_created()->date( 'H:i:s' ) ) : '',
			'modified_date_time'     => $order->get_date_modified() ? $this->get_shipstation_date_format( $order->get_date_modified()->date( 'Y-m-d' ), $order->get_date_modified()->date( 'H:i:s' ) ) : '',
		);

		/**
		 * Filter to allow modification of the order data before it is returned.
		 *
		 * This filter is useful for adding more information to the order data such as :
		 * - `tax_identifier`
		 * - `original_order_source`
		 * - `fulfilled_date`
		 *
		 * For more information on all available parameter,
		 * please refer to this ShipStation API documentation : https://ddnmn7gngv.apidog.io/orders-18958392e0.
		 *
		 * @param array    $order_data The order data to be returned.
		 * @param WC_Order $order      The order object.
		 *
		 * @since 5.0.0
		 */
		return apply_filters( 'dokan_rest_shipstation_orders_controller_get_order_data', $order_data, $order );
	}

	/**
	 * Get buyer information from order.
	 *
	 * @param WC_Order $order Order object.
	 *
	 * @return array
	 */
	public function get_buyer( WC_Order $order ): array {
		$buyer = $order->get_user();

		if ( false !== $buyer ) {
			return array(
				'buyer_id' => $buyer->user_login,
				'name'     => $buyer->user_firstname . ' ' . $buyer->user_lastname,
				'email'    => $buyer->user_email,
				'phone'    => $order->get_billing_phone(),
			);
		}

		return array(
			'buyer_id' => $order->get_billing_email(),
			'name'     => $order->get_billing_first_name() . ' ' . $order->get_billing_last_name(),
			'email'    => $order->get_billing_email(),
			'phone'    => $order->get_billing_phone(),
		);
	}

	/**
	 * Get Payment info for the order data.
	 *
	 * @param WC_Order $order      Order object.
	 * @param array    $extra_args Extra args.
	 *
	 * @return array
	 */
	public function get_payment_info( WC_Order $order, array $extra_args ): array {
		$order_total = $order->get_total() - floatval( $order->get_total_refunded() );

		// Maybe convert the order total using per-order exchange rate.
		$rate = $this->get_exchange_rate();
		if ( 1.00 !== $rate ) {
			$order_total = floatval( $order_total * $rate );
		}

		$payment_info = array(
			'payment_status'   => $this->get_payment_status( $order->get_status() ),
			'taxes'            => $this->get_taxes( $order->get_taxes(), $extra_args ),
			'shipping_charges' => $this->get_shipping_charges( $order->get_shipping_methods(), $extra_args ),
			'amount_paid'      => $order_total,
			'payment_method'   => $order->get_payment_method(),
		);

		if ( ! empty( $order->get_coupons() ) && is_array( $order->get_coupons() ) ) {
			foreach ( $order->get_coupons() as $coupon ) {
				if ( ! $coupon instanceof WC_Order_Item_Coupon ) {
					continue;
				}

				$payment_info['coupon_codes'][] = $coupon->get_code();
				$payment_info['adjustments'][]  = array(
					// translators: %1$s is a coupon code.
					'description' => sprintf( __( 'Discount from coupon: %1$s', 'dokan' ), $coupon->get_code() ),
					'amount'      => $coupon->get_discount(),
				);
			}
		}

		return $payment_info;
	}

	/**
	 * Get the payment status for ShipStation based on WooCommerce order status.
	 *
	 * @param string $order_status The WooCommerce order status.
	 *
	 * @return string
	 */
	public function get_payment_status( string $order_status ): string {
		// Map WooCommerce order status to ShipStation payment status.
		switch ( $order_status ) {
			case 'pending':
			case 'on-hold':
				return self::AWAITING_PAYMENT_STATUS;
			case 'cancelled':
				return self::CANCELLED_STATUS;
			case 'refunded':
				return self::PAYMENT_CANCELLED_STATUS;
			case 'failed':
				return self::PAYMENT_FAILED_STATUS;
			default:
				return self::PAID_STATUS;
		}
	}

	/**
	 * Get the taxes for the order.
	 *
	 * @param array $order_taxes Order taxes.
	 * @param array $extra_args  Extra args.
	 *
	 * @return array
	 */
	public function get_taxes( array $order_taxes, array $extra_args ): array {
		$taxes = array();

		foreach ( $order_taxes as $tax ) {
			if ( ! $tax instanceof WC_Order_Item_Tax ) {
				continue; // Skip if not a valid order item tax.
			}

			$tax_amount = floatval( $tax->get_tax_total() ) + floatval( $tax->get_shipping_tax_total() );

			// Maybe convert the tax amount using per-order exchange rate.
			$rate = $this->get_exchange_rate();
			if ( 1.00 !== $rate ) {
				$tax_amount = floatval( $tax_amount * $rate );
			}

			$taxes[] = array(
				'amount'      => $tax_amount,
				'description' => $tax->get_name(),
			);
		}

		return $taxes;
	}

	/**
	 * Get the shipping charges for the order.
	 *
	 * @param array $order_shipping_methods Order shipping methods.
	 * @param array $extra_args Extra args.
	 *
	 * @return array
	 */
	public function get_shipping_charges( $order_shipping_methods, $extra_args ): array {
		$shipping_charges = array();

		foreach ( $order_shipping_methods as $shipping_method ) {
			if ( ! $shipping_method instanceof \WC_Order_Item_Shipping ) {
				continue; // Skip if not a valid order item shipping.
			}

			$shipping_amount = floatval( $shipping_method->get_total() );

			// Maybe convert the shipping amount using per-order exchange rate.
			$rate = $this->get_exchange_rate();
			if ( 1.00 !== $rate ) {
				$shipping_amount = floatval( $shipping_amount * $rate );
			}

			$shipping_charges[] = array(
				'amount'      => $shipping_amount,
				'description' => $shipping_method->get_method_title(),
			);
		}

		return $shipping_charges;
	}

	/**
	 * Get the date format expected by ShipStation.
	 *
	 * @param string $date The date in 'Y-m-d' format.
	 * @param string $time The time in 'H:i:s' format.
	 *
	 * @return string
	 */
	public function get_shipstation_date_format( string $date, string $time ): string {
		if ( ! $date || ! $time ) {
			return '';
		}

		// Format the date and time to match ShipStation's expected format.
		return sprintf( '%1$sT%2$s.000Z', $date, $time );
	}

	/**
	 * Get requested fulfillments for the order.
	 *
	 * @param WC_Order $order      The order object.
	 * @param array    $extra_args Extra args.
	 *
	 * @return array
	 */
	public function get_requested_fulfillments( WC_Order $order, array $extra_args ): array {
		$fulfillments      = array();
		$fulfillment_items = array();
		$order_items       = $order->get_items() + $order->get_items( 'fee' );

		foreach ( $order_items as $item_id => $item ) {
			$fulfillment_item = $this->get_fulfillment_item( intval( $item_id ), $order, 0, $extra_args );

			if ( empty( $fulfillment_item ) ) {
				continue;
			}

			$fulfillment_items[] = $fulfillment_item;
		}

		if ( empty( $fulfillment_items ) ) {
			return $fulfillments;
		}

		// Append cart level discount line.
		$fulfillment_items = $this->maybe_append_discount_line( $fulfillment_items, $order, $extra_args );

		$fulfillments[] = $this->get_fulfillment( $fulfillment_items, $order );

		return $fulfillments;
	}

	/**
	 * Get fulfillment info.
	 *
	 * @param array    $fulfillment_items Fulfillment items.
	 * @param WC_Order $order             Order object.
	 * @param string   $fulfillment_id    WC Fulfillment ID.
	 *
	 * @return array
	 */
	public function get_fulfillment( array $fulfillment_items, WC_Order $order, string $fulfillment_id = '' ): array {

		$gift = $this->get_gift( $order );

		// Fall back to billing address when shipping address is not set
		$shipping_country = $order->get_shipping_country();
		$shipping_address = $order->get_shipping_address_1();

		if ( empty( $shipping_country ) && empty( $shipping_address ) ) {
			$address_data = array(
				'name'     => $order->get_billing_first_name() . ' ' . $order->get_billing_last_name(),
				'company'  => $order->get_billing_company(),
				'phone'    => $order->get_billing_phone(),
				'address1' => $order->get_billing_address_1(),
				'address2' => $order->get_billing_address_2(),
				'city'     => $order->get_billing_city(),
				'state'    => $order->get_billing_state(),
				'postcode' => $order->get_billing_postcode(),
				'country'  => $order->get_billing_country(),
			);
		} else {
			$address_data = array(
				'name'     => $order->get_shipping_first_name() . ' ' . $order->get_shipping_last_name(),
				'company'  => $order->get_shipping_company(),
				'phone'    => $order->get_billing_phone(),
				'address1' => $order->get_shipping_address_1(),
				'address2' => $order->get_shipping_address_2(),
				'city'     => $order->get_shipping_city(),
				'state'    => $order->get_shipping_state(),
				'postcode' => $order->get_shipping_postcode(),
				'country'  => $order->get_shipping_country(),
			);
		}

		return array(
			'requested_fulfillment_id' => $fulfillment_id,
			'ship_to'                  => array(
				'name'           => $address_data['name'],
				'company'        => $address_data['company'],
				'phone'          => $address_data['phone'],
				'address_line_1' => $address_data['address1'],
				'address_line_2' => $address_data['address2'],
				'city'           => $address_data['city'],
				'state_province' => $address_data['state'],
				'postal_code'    => $address_data['postcode'],
				'country_code'   => $address_data['country'],
			),
			'items'                    => $fulfillment_items,
			'extensions'               => $this->get_custom_fields( $order ),
			'shipping_preferences'     => array(
				'gift' => $gift['is_gift'],
			),
		);
	}

	/**
	 * Append a cart-level discount line to the fulfillment items when enabled.
	 *
	 * @param array      $items      Current items array.
	 * @param WC_Order   $order      Order object.
	 * @param array      $extra_args Extra args including exchange_rate and export_discounts_as_separate_item.
	 * @param float|null $amount     Optional precomputed discount amount (already in target currency, positive value).
	 *
	 * @return array Updated items array.
	 */
	private function maybe_append_discount_line( array $items, WC_Order $order, array $extra_args, ?float $amount = null ): array {
		if ( ! $this->should_export_discounts_as_separate_item() || ! $order->get_total_discount() ) {
			return $items;
		}

		if ( null === $amount ) {
			$order_total_discount = $order->get_total_discount() * -1;
			// Maybe convert order total discount using per-order exchange rate.
			$rate = $this->get_exchange_rate();
			if ( 1.00 !== $rate ) {
				$order_total_discount = floatval( $order_total_discount * $rate );
			}
			$discount_value = NumberUtil::round( $order_total_discount, wc_get_price_decimals() );
		} else {
			$discount_value = NumberUtil::round( floatval( $amount ), wc_get_price_decimals() ) * -1;
		}

		$items[] = array(
			'description' => __( 'Total Discount', 'dokan' ),
			'quantity'    => 1,
			'unit_price'  => $discount_value,
		);

		return $items;
	}

	/**
	 * Get fulfillment item from WC Fufillment.
	 *
	 * @param int      $item_id    Order item ID.
	 * @param WC_Order $order      Order object.
	 * @param int      $quantity   Item quantity.
	 * @param array    $extra_args Extra args.
	 */
	public function get_fulfillment_item( $item_id, $order, $quantity = 0, $extra_args = array() ) {
		$item             = $order->get_item( $item_id );
		$fulfillment_item = array();

		if ( ! $item instanceof WC_Order_Item ) {
			return $fulfillment_item;
		}

		$is_order_item_a_product = $item instanceof WC_Order_Item_Product;
		$product                 = $is_order_item_a_product ? $item->get_product() : false;
		$item_needs_no_shipping  = ! $product || ! $product->needs_shipping();
		$item_not_a_fee          = 'fee' !== $item->get_type();

		/**
		 * Allow third party to exclude the item for when an item does not need shipping or is a fee.
		 *
		 * @since 5.0.0
		 */
		if ( apply_filters( 'dokan_shipstation_no_shipping_item', $item_needs_no_shipping && $item_not_a_fee, $product, $item ) ) {
			return $fulfillment_item;
		}

		$unit_price        = 0;
		$discount_per_unit = 0;
		$discount_per_line = 0;
		$item_url          = '';
		$item_product      = array();
		$item_taxes        = array();

		if ( 'fee' === $item->get_type() ) {
			$quantity   = 1;
			$unit_price = $order->get_item_total( $item, false, true );

			// Maybe convert fee item total using per-order exchange rate.
			$rate = $this->get_exchange_rate();
			if ( 1.00 !== $rate ) {
				$unit_price = floatval( $unit_price * $rate );
			}
		}

		// handle product specific data.
		if ( $is_order_item_a_product && $product && $product->needs_shipping() ) {
			/**
			 * Handle product specific data.
			 *
			 * @var WC_Order_Item_Product $item
			 */
			$product_id   = $item->get_variation_id() ? $item->get_variation_id() : $item->get_product_id();
			$item_url     = $product->get_permalink();
			$unit_cost    = is_callable( array( $item, 'get_cogs_value' ) ) ? $item->get_cogs_value() : 0;
			$item_product = array(
				'product_id'  => $product_id,
				'name'        => $item->get_name(),
				'description' => $product->get_description(),
				'identifiers' => array(
					'sku'             => $product->get_sku(),
					'isbn'            => $product->get_global_unique_id(),
					'fulfillment_sku' => isset( $extra_args['fulfillment_id'] ) ? $extra_args['fulfillment_id'] : '',
				),
				'details'     => $this->get_item_details( $item ),
				'unit_cost'   => $unit_cost,
				'weight'      => $this->get_item_weight( $product ),
				'dimensions'  => $this->get_item_dimensions( $product ),
				'urls'        => array(
					'image_url'     => $this->get_image_src_url( $product->get_image_id(), 'full' ),
					'product_url'   => $product->get_permalink(),
					'thumbnail_url' => $this->get_image_src_url( $product->get_image_id(), 'woocommerce_thumbnail' ),
				),
			);

			$item_taxes = $item->get_taxes();

			if ( 0 === $quantity ) {
				$quantity = $item->get_quantity() - abs( $order->get_qty_refunded_for_item( $item_id ) );
			}

			$unit_price        = $this->should_export_discounts_as_separate_item() ? $order->get_item_subtotal( $item, false, true ) : $order->get_item_total( $item, false, true );
			$discount_per_unit = floatval( $order->get_item_subtotal( $item, false, true ) - floatval( $order->get_item_total( $item, false, true ) ) );
			$discount_per_line = $discount_per_unit * $quantity;

			// Maybe convert item total using per-order exchange rate.
			$rate = $this->get_exchange_rate();
			if ( 1.00 !== $rate ) {
				$unit_price        = floatval( $unit_price * $rate );
				$discount_per_unit = floatval( $discount_per_unit * $rate );
				$discount_per_line = floatval( $discount_per_line * $rate );
			}
		}

		if ( 0 === $quantity ) {
			return $fulfillment_item;
		}

		$fulfillment_item = array_filter(
			array(
				'line_item_id'       => $item_id,
				'description'        => $item->get_name(),
				// Fee lines carry no product data. Passing an empty array here would be
				// serialized as a JSON array (`[]`) and ShipStation fails to deserialize it
				// into its Product object, so omit the key entirely instead.
				'product'            => ! empty( $item_product ) ? $item_product : null,
				'quantity'           => $quantity,
				'unit_price'         => $unit_price,
				'discount_per_unit'  => $discount_per_unit,
				'discount_per_line'  => $discount_per_line,
				'taxes'              => $this->get_item_taxes( $item_taxes ),
				'item_url'           => $item_url,
				'modified_date_time' => $order->get_date_modified() ? $this->get_shipstation_date_format( $order->get_date_modified()->date( 'Y-m-d' ), $order->get_date_modified()->date( 'H:i:s' ) ) : '',
			),
			function ( $value ) {
				return $value !== '' && $value !== null;
			}
		);

		/**
		 * Filters the fulfillment item data before it is returned.
		 *
		 * This filter allows third-party plugins to modify the fulfillment item data
		 * that will be sent to ShipStation.
		 *
		 * @since 5.0.0
		 *
		 * @param array         $fulfillment_item The fulfillment item data.
		 * @param WC_Order      $order            The order object.
		 * @param WC_Order_Item $item             The order item data.
		 * @param array         $extra_args       Extra arguments passed to the method.
		 */
		return apply_filters( 'dokan_rest_shipstation_fulfillment_item', $fulfillment_item, $order, $item, $extra_args );
	}

	/**
	 * Get item weight.
	 *
	 * @param \WC_Product $product WooCommerce Product Object.
	 *
	 * @return array
	 */
	public function get_item_weight( $product ) {
		$weight_unit    = strtolower( get_option( 'woocommerce_weight_unit' ) );
		$ss_weight_unit = 'Kilogram';

		switch ( $weight_unit ) {
			case 'g':
				$ss_weight_unit = 'Gram';
				break;
			case 'lbs':
				$ss_weight_unit = 'Pound';
				break;
			case 'oz':
				$ss_weight_unit = 'Ounce';
				break;
			default:
				$ss_weight_unit = 'Kilogram';
				$weight_unit    = 'kg'; // Make sure the weight unit is consistent.
				break;
		}

		return array(
			'unit'  => $ss_weight_unit,
			'value' => wc_get_weight( floatval( $product->get_weight() ), $weight_unit ),
		);
	}

	/**
	 * Get item dimensions.
	 *
	 * @param \WC_Product $product WooCommerce Product Object.
	 *
	 * @return array
	 */
	public function get_item_dimensions( $product ) {
		$dimension_unit = strtolower( get_option( 'woocommerce_dimension_unit' ) );

		switch ( $dimension_unit ) {
			case 'in':
				$ss_dimension_unit = 'Inch';
				break;
			default:
				$ss_dimension_unit = 'Centimeter';
				$dimension_unit    = 'cm';
				break;
		}

		return array(
			'length' => wc_get_dimension( floatval( $product->get_length() ), $dimension_unit ),
			'width'  => wc_get_dimension( floatval( $product->get_width() ), $dimension_unit ),
			'height' => wc_get_dimension( floatval( $product->get_height() ), $dimension_unit ),
			'unit'   => $ss_dimension_unit,
		);
	}

	/**
	 * Get taxes information from the item.
	 *
	 * @param array $item_taxes Item Taxes.
	 *
	 * @return array
	 */
	public function get_item_taxes( $item_taxes ) {
		$taxes = array();

		if ( ! is_array( $item_taxes ) || empty( $item_taxes['total'] ) ) {
			return $taxes;
		}

		foreach ( $item_taxes['total'] as $rate_id => $rate_value ) {
			$tax_label = WC_Tax::get_rate_label( $rate_id );

			$taxes[] = array(
				'amount'      => floatval( $rate_value ),
				'description' => ! empty( $tax_label ) ? $tax_label : __( 'Tax', 'dokan' ),
			);
		}

		return $taxes;
	}

	/**
	 * Get gift info.
	 *
	 * @param WC_Order $order Order object.
	 *
	 * @return array
	 */
	public function get_gift( $order ) {
		$gift = array(
			'is_gift'      => false,
			'gift_message' => '',
		);

		if ( $order->get_meta( self::$order_meta_keys['is_gift'] ) ) {
			$gift['is_gift'] = true;

			$gift_message = $order->get_meta( self::$order_meta_keys['gift_message'] );

			if ( ! empty( $gift_message ) ) {
				$gift['gift_message'] = wp_specialchars_decode( $gift_message );
			}
		}

		return $gift;
	}

	/**
	 * Get a list of notes.
	 *
	 * @param WC_Order $order Order object.
	 *
	 * @return array
	 */
	public function get_notes( WC_Order $order ): array {

		$notes = array();

		if ( ! empty( $order->get_customer_note() ) ) {
			$notes[] = array(
				'type' => 'NotesFromBuyer',
				'text' => $order->get_customer_note(),
			);
		}

		$gift = $this->get_gift( $order );

		if ( ! empty( $gift['gift_message'] ) ) {
			$notes[] = array(
				'type' => 'GiftMessage',
				'text' => $gift['gift_message'],
			);
		}

		$internal_notes = array();

		// Add internal order note for orders that are marked as gift.
		if ( $gift['is_gift'] ) {
			$internal_notes[] = __( 'This order is marked as a gift.', 'dokan' );
		}

		// Add internal order note for orders with discounts.
		if ( $order->get_total_discount() > 0 ) {
			/* translators: %s: discount amount */
			$internal_notes[] = sprintf( __( 'This order has a total discount of %s.', 'dokan' ), wc_price( $order->get_total_discount(), array( 'currency' => $order->get_currency() ) ) );
		}

		if ( ! empty( $internal_notes ) ) {
			$notes[] = array(
				'type' => 'InternalNotes',
				'text' => implode( ' | ', $internal_notes ),
			);
		}

		return $notes;
	}
	/**
	 * Get custom fields
	 *
	 * @param WC_Order $order Order object.
	 *
	 * @return array
	 */
	public function get_custom_fields( $order ) {
		$custom_fields = array();

		// Custom fields - 1 is used for coupon codes.
		$custom_fields['custom_field_1'] = implode( ' | ', $order->get_coupon_codes() );

		// Custom fields 2 and 3 can be mapped to a custom field via the following filters.

		/**
		 * Custom fields 2 can be mapped to a custom field via the following filters.
		 *
		 * @since 5.0.0
			 *
			 * @param string $meta_key The meta key to map to custom field 2.
			 * @return string The meta key to map to custom field 2.
			 *
			 * @since 5.0.0
		 */
		$meta_key = apply_filters( 'dokan_shipstation_export_custom_field_2', '' );
		if ( $meta_key ) {
			/**
			 * Allowing third party to modify the custom field 2 value.
			 *
			 * @since 5.0.0
			 */
			$custom_fields['custom_field_2'] = apply_filters( 'dokan_shipstation_export_custom_field_2_value', $order->get_meta( $meta_key, true ), $order->get_id() );
		}

		/**
		 * Custom fields 3 can be mapped to a custom field via the following filters.
		 *
		 * @since 5.0.0
		 */
		$meta_key = apply_filters( 'dokan_shipstation_export_custom_field_3', '' );
		if ( $meta_key ) {
			/**
			 * Allowing third party to modify the custom field 3 value.
			 *
			 * @since 5.0.0
			 */
			$custom_fields['custom_field_3'] = apply_filters( 'dokan_shipstation_export_custom_field_3_value', $order->get_meta( $meta_key, true ), $order->get_id() );
		}

		return $custom_fields;
	}

	/**
	 * Get item details for the order.
	 *
	 * @param WC_Order_Item $item Order item.
	 *
	 * @return array
	 */
	public function get_item_details( $item ): array {
		$item_details = array();

		add_filter( 'woocommerce_is_attribute_in_product_name', '__return_false' );
		$formatted_meta = $item->get_formatted_meta_data();

		if ( empty( $formatted_meta ) ) {
			return $item_details;
		}

		foreach ( $formatted_meta as $meta_key => $meta ) {
			$item_details[] = array(
				'name'  => $meta->display_key,
				'value' => wp_strip_all_tags( $meta->display_value ),
			);
		}

		return $item_details;
	}

	/**
	 * Get the image source URL for a given image ID and size.
	 *
	 * @param int    $image_id Image ID.
	 * @param string $size     Image size (default is 'full').
	 *
	 * @return string
	 */
	public function get_image_src_url( $image_id, $size = 'full' ): string {
		if ( ! $image_id ) {
			return '';
		}

		$image_src = wp_get_attachment_image_src( $image_id, $size );

		if ( ! is_array( $image_src ) || empty( $image_src[0] ) ) {
			return '';
		}

		return esc_url( $image_src[0] );
	}

	/**
	 * Get the vendor's store address for the ship_from field.
	 *
	 * Falls back to WooCommerce base store address if vendor address is not set.
	 *
	 * @return array
	 */
	private function get_vendor_ship_from_address(): array {
		$vendor  = dokan()->vendor->get( $this->get_vendor_id() );
		$address = $vendor->get_address();

		$store_name  = $vendor->get_shop_name();
		$store_phone = $vendor->get_phone();

		return array(
			'name'           => ! empty( $store_name ) ? $store_name : '',
			'company'        => ! empty( $store_name ) ? $store_name : '',
			'phone'          => ! empty( $store_phone ) ? $store_phone : '',
			'address_line_1' => ! empty( $address['street_1'] ) ? $address['street_1'] : WC()->countries->get_base_address(),
			'address_line_2' => ! empty( $address['street_2'] ) ? $address['street_2'] : WC()->countries->get_base_address_2(),
			'address_line_3' => '',
			'city'           => ! empty( $address['city'] ) ? $address['city'] : WC()->countries->get_base_city(),
			'state_province' => ! empty( $address['state'] ) ? $address['state'] : WC()->countries->get_base_state(),
			'postal_code'    => ! empty( $address['zip'] ) ? $address['zip'] : WC()->countries->get_base_postcode(),
			'country_code'   => ! empty( $address['country'] ) ? $address['country'] : WC()->countries->get_base_country(),
		);
	}

	/**
	 * Update orders shipments for specified SKUs (both products and variations).
	 *
	 * @param WP_REST_Request $request Request object.
	 *
	 * @return WP_REST_Response
	 */
	public function update_orders_shipments( WP_REST_Request $request ): WP_REST_Response {
		$request_params = $request->get_json_params();
		$notifications  = isset( $request_params['notifications'] ) && is_array( $request_params['notifications'] ) ? $request_params['notifications'] : array();

		if ( empty( $notifications ) || ! is_array( $notifications ) ) {
			return new WP_REST_Response( 'Invalid request format.', 400 );
		}

		$response = array();

		foreach ( $notifications as $notification ) {
			$saved_notification = array(
				'notification_id'  => '',
				'tracking_number'  => '',
				'tracking_url'     => '',
				'carrier_code'     => '',
				'ext_location_id'   => '',
				'items'            => array(),
				'ship_to'          => array(),
				'ship_from'        => array(),
				'return_address'   => array(),
				'ship_date'        => '',
				'currency'         => '',
				'fulfillment_cost' => 0.0,
				'insurance_cost'   => 0.0,
				'notify_buyer'     => false,
				'notes'            => array(),
			);

			if ( empty( $notification['notification_id'] ) ) {
				self::log( __( 'Notification ID is empty for this notification: ', 'dokan' ) . print_r( $notification, true ) );// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r --- Its needed for logging
				continue; // Skip if notification ID is not set.
			}

			if ( empty( $notification['order_id'] ) || empty( $notification['items'] ) || ! is_array( $notification['items'] ) ) {
				$response[] = array(
					'notification_id' => $notification['notification_id'],
					'status'          => 'failure',
					'failure_reason'  => __( 'Empty order ID or items', 'dokan' ),
				);

				// translators: %1$s is the notification id.
				self::log( sprintf( __( 'Notification ID: %1$s doesnt have order ID or items.', 'dokan' ), $notification['notification_id'] ) );

				continue; // Skip invalid items.
			}

			if ( ! is_numeric( $notification['order_id'] ) ) {
				$response[] = array(
					'notification_id' => $notification['notification_id'],
					'status'          => 'failure',
					'failure_reason'  => __( 'Order ID is not numeric', 'dokan' ),
				);

				// translators: %1$s is the order id, %2$d is the notification id.
				self::log( sprintf( __( 'Order ID: %1$d from notification ID: %2$s is not numeric.', 'dokan' ), $notification['order_id'], $notification['notification_id'] ) );

				continue; // Skip if product_id is not numeric.
			}

			$order_id     = absint( $notification['order_id'] );
			$order_number = '';
			$order        = wc_get_order( $order_id );

			// Fallback: try order number if order ID lookup failed.
			if ( ! $order instanceof WC_Order ) {
				$order_number = isset( $notification['order_number'] ) ? (string) $notification['order_number'] : '';
				$order        = wc_get_order( $order_number );
			}

			// Skip if order still not found.
			if ( ! $order instanceof WC_Order ) {
				$response[] = array(
					'notification_id' => $notification['notification_id'],
					'status'          => 'failure',
					'failure_reason'  => __( 'Order not found', 'dokan' ),
				);

				continue;
			}

			// Verify the order belongs to the authenticated vendor.
			$order_vendor_id = (int) dokan_get_seller_id_by_order( $order->get_id() );
			if ( $order_vendor_id !== $this->get_vendor_id() ) {
				$response[] = array(
					'notification_id' => $notification['notification_id'],
					'status'          => 'failure',
					'failure_reason'  => __( 'Order does not belong to this vendor', 'dokan' ),
				);

				continue;
			}

			// Sanitize each field individually instead of merging raw input via wp_parse_args().
			$saved_notification['notification_id']  = sanitize_text_field( $notification['notification_id'] );
			$saved_notification['tracking_number']  = ! empty( $notification['tracking_number'] ) ? sanitize_text_field( $notification['tracking_number'] ) : '';
			$saved_notification['tracking_url']     = ! empty( $notification['tracking_url'] ) ? esc_url_raw( $notification['tracking_url'] ) : '';
			$saved_notification['carrier_code']     = ! empty( $notification['carrier_code'] ) ? sanitize_text_field( $notification['carrier_code'] ) : '';
			$saved_notification['ext_location_id']   = ! empty( $notification['ext_location_id'] ) ? sanitize_text_field( $notification['ext_location_id'] ) : '';
			$saved_notification['currency']         = ! empty( $notification['currency'] ) ? sanitize_text_field( $notification['currency'] ) : '';
			$saved_notification['ship_to']          = ! empty( $notification['ship_to'] ) ? $this->parse_address_info( $notification['ship_to'] ) : array();
			$saved_notification['ship_from']        = ! empty( $notification['ship_from'] ) ? $this->parse_address_info( $notification['ship_from'] ) : array();
			$saved_notification['return_address']   = ! empty( $notification['return_address'] ) ? $this->parse_address_info( $notification['return_address'] ) : array();
			$saved_notification['ship_date']        = ! empty( $notification['ship_date'] ) && strtotime( $notification['ship_date'] ) ? gmdate( 'Y-m-d H:i:s', strtotime( $notification['ship_date'] ) ) : '';
			$saved_notification['fulfillment_cost'] = ! empty( $notification['fulfillment_cost'] ) ? floatval( $notification['fulfillment_cost'] ) : 0.0;
			$saved_notification['insurance_cost']   = ! empty( $notification['insurance_cost'] ) ? floatval( $notification['insurance_cost'] ) : 0.0;
			$saved_notification['notify_buyer']     = ! empty( $notification['notify_buyer'] ) ? filter_var( $notification['notify_buyer'], FILTER_VALIDATE_BOOLEAN ) : false;
			$saved_notification['notes']            = ! empty( $notification['notes'] ) ? $this->parse_notes( $notification['notes'] ) : array();

			$saved_items = array();

			foreach ( $notification['items'] as $item ) {
				if ( empty( $item['description'] ) && empty( $item['quantity'] ) ) {
					self::log( __( 'Skipped this item because doesnt have description and quantity: ', 'dokan' ) . print_r( $item, true ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r --- Its needed for logging
					continue; // Skip if required fields are not set.
				}

				$saved_item = array(
					'description'  => isset( $item['description'] ) ? sanitize_text_field( $item['description'] ) : '',
					'quantity'     => isset( $item['quantity'] ) ? absint( $item['quantity'] ) : 0,
					'line_item_id' => isset( $item['line_item_id'] ) ? sanitize_text_field( $item['line_item_id'] ) : '',
					'sku'          => isset( $item['sku'] ) ? sanitize_text_field( $item['sku'] ) : '',
					'product_id'   => isset( $item['product_id'] ) ? sanitize_text_field( $item['product_id'] ) : '',
				);

				$order_item = $order->get_item( $saved_item['line_item_id'] );

				if ( $order_item instanceof WC_Order_Item_Product ) {
					$order_item_product        = $order_item->get_product();
					$saved_item['description'] = $order_item->get_name();
					$saved_item['sku']         = $order_item_product->get_sku();
					$saved_item['product_id']  = $order_item->get_id();
				}

				$saved_items[] = $saved_item;

				self::log( __( 'ShipNotify Item: ', 'dokan' ) . print_r( $saved_item, true ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r --- Its needed for logging
			}

			if ( ! empty( $saved_items ) ) {
				$saved_notification['items'] = $saved_items;
				$this->process_items( $saved_items, $order, $saved_notification );
			}

			$response[] = array(
				'notification_id' => $saved_notification['notification_id'],
				'status'          => 'success',
				'order_id'        => $order->get_id(),
			);
		}

		return new WP_REST_Response(
			array(
				'notification_results' => $response,
			),
			200
		);
	}

	/**
	 * Process items from the ShipStation notification.
	 *
	 * @param array    $items        Items from the notification.
	 * @param WC_Order $order        The order object.
	 * @param array    $notification The notification data.
	 *
	 * @return void
	 */
	public function process_items( array $items, WC_Order $order, array $notification ) {
		$shipped_items      = array();
		$shipped_item_count = 0;
		$order_shipped      = false;
		$timestamp          = false !== strtotime( $notification['ship_date'] ) ? strtotime( $notification['ship_date'] ) : wp_date( 'U' );
		$tracking_number    = ! empty( $notification['tracking_number'] ) ? wc_clean( wp_unslash( $notification['tracking_number'] ) ) : '';
		$tracking_url       = ! empty( $notification['tracking_url'] ) ? wc_clean( wp_unslash( $notification['tracking_url'] ) ) : '';
		$carrier            = ! empty( $notification['carrier_code'] ) ? wc_clean( wp_unslash( $notification['carrier_code'] ) ) : '';

		foreach ( $items as $item ) {
			$item_sku    = wc_clean( (string) $item['sku'] );
			$item_name   = wc_clean( (string) $item['description'] );
			$qty_shipped = absint( $item['quantity'] );

			if ( $item_sku ) {
				$item_sku = ' (' . $item_sku . ')';
			}

			$item_id = absint( $item['line_item_id'] );
			$order_item_obj = $order->get_item( $item_id );
			if ( $order_item_obj instanceof WC_Order_Item_Product ) {
				$product = $order_item_obj->get_product();
				if ( $product && $product->is_virtual() ) {
					/* translators: 1: item name */
					self::log( sprintf( __( 'Item %s is not shippable product. Skipping.', 'dokan' ), $item_name ) );
					continue;
				}
			}

			$shipped_item_count += $qty_shipped;
			$shipped_items[]     = $item_name . $item_sku . ' x ' . $qty_shipped;
		}

		// Number of shippable items in WC order (excludes virtual/downloadable products).
		$total_item_count = 0;
		foreach ( $order->get_items() as $order_item ) {
			if ( $order_item instanceof WC_Order_Item_Product ) {
				$product = $order_item->get_product();
				if ( $product && ! $product->is_virtual() ) {
					$total_item_count += $order_item->get_quantity();
				}
			}
		}

		// If we have a list of shipped items, we can customise the note + see
		// if the order is not yet complete.
		if ( count( $shipped_items ) > 0 ) {
			$order_note = sprintf(
				/* translators: 1) shipped items 2) carrier's name 3) shipped date, 4) tracking number */
				__( '%1$s shipped via %2$s on %3$s with tracking number %4$s.', 'dokan' ),
				esc_html( implode( ', ', $shipped_items ) ),
				esc_html( $carrier ),
				date_i18n( get_option( 'date_format' ), $timestamp ),
				$tracking_number
			);

			$current_shipped_items = max( (int) $order->get_meta( '_shipstation_shipped_item_count', true ), 0 );

			if ( ( $current_shipped_items + $shipped_item_count ) >= $total_item_count ) {
				$order_shipped = true;
			}

			self::log(
				sprintf(
					/* translators: 1) number of shipped items 2) total shipped items 3) order ID */
					__( 'Shipped %1$d out of %2$d items in order %3$s', 'dokan' ),
					$shipped_item_count,
					$total_item_count,
					$order->get_id()
				)
			);

			$order->update_meta_data( '_shipstation_shipped_item_count', $current_shipped_items + $shipped_item_count );
			$order->save_meta_data();
		} else {
			// If we don't have items from SS and order items in WC.
			$order_shipped = 0 === $total_item_count;

			$order_note = sprintf(
				/* translators: 1) carrier's name 2) shipped date, 3) tracking number */
				__( 'Items shipped via %1$s on %2$s with tracking number %3$s (Shipstation).', 'dokan' ),
				esc_html( $carrier ),
				date_i18n( get_option( 'date_format' ), $timestamp ),
				$tracking_number
			);

			/* translators: 1: order id */
			self::log( sprintf( __( 'No items found - shipping entire order %d.', 'dokan' ), $order->get_id() ) );
		}

		$current_status = 'wc-' . $order->get_status();

		// Tracking information - WC Shipment Tracking extension.
		if ( class_exists( 'WC_Shipment_Tracking' ) ) {
			if ( function_exists( 'wc_st_add_tracking_number' ) ) {
				wc_st_add_tracking_number( $order->get_id(), $tracking_number, strtolower( $carrier ), $timestamp );
			} else {
				$order->update_meta_data( '_tracking_provider', strtolower( $carrier ) );
				$order->update_meta_data( '_tracking_number', $tracking_number );
				$order->update_meta_data( '_date_shipped', $timestamp );
				$order->save_meta_data();
				self::log( __( 'You\'re using Shipment Tracking < 1.4.0. Please update!', 'dokan' ) );
			}
			$is_customer_note = false;
		} else {
			$is_customer_note = $this->get_shipped_status() !== $current_status;
		}

		$tracking_data = array(
			'tracking_number' => $tracking_number,
			'carrier'         => $carrier,
			'ship_date'       => $timestamp,
			'data'            => $notification,
			'xml'             => '',
		);

		/**
		* Allow to override tracking note.
		*
		* @param string   $order_note
		* @param WC_Order $order
		* @param array    $tracking_data
		*
		* @since 5.0.0
		*/
		$order_note = apply_filters(
			'dokan_rest_shipstation_shipnotify_tracking_note',
			$order_note,
			$order,
			$tracking_data
		);

		$order->add_order_note(
			$order_note,
			/**
			* Allow to override should tracking note be sent to customer.
			*
			* @param bool     $is_customer_note
			* @param string   $order_note
			* @param WC_Order $order
			* @param array    $tracking_data
			*
			* @since 5.0.0
			*/
			apply_filters(
				'dokan_rest_shipstation_shipnotify_send_tracking_note',
				$is_customer_note,
				$order_note,
				$order,
				$tracking_data
			)
		);

		/**
		 * Trigger action for other integrations.
		 *
		 * @param WC_Order $order         Order object.
		 * @param array    $tracking_data Tracking data.
		 *
		 * @since 5.0.0
		 */
		do_action(
			'dokan_shipstation_shipnotify',
			$order,
			$tracking_data
		);

		// Update order status.
		if (
			/**
			* Allow to override is order shipped flag.
			*
			* @param bool     $order_shipped
			* @param WC_Order $order
			* @param array    $tracking_data
			*
			* @since 5.0.0
			*/
			apply_filters(
				'dokan_shipstation_shipnotify_order_shipped',
				$order_shipped,
				$order,
				$tracking_data
			)
			&& $this->get_shipped_status() !== $current_status
		) {
			$order->update_status( $this->get_shipped_status() );

			/* translators: 1) order ID 2) shipment status */
			self::log( sprintf( __( 'Updated order %1$s to status %2$s', 'dokan' ), $order->get_id(), $this->get_shipped_status() ) );

			/**
			 * Trigger action after the order status is changed for other integrations.
			 *
			 * @param WC_Order $order         Order object.
			 * @param array    $tracking_data Tracking data.
			 *
			 * @since 5.0.0
			 */
			do_action(
				'dokan_shipstation_shipnotify_status_updated',
				$order,
				$tracking_data
			);
		}
	}

	/**
	 * Parse notes and return a standardized array.
	 *
	 * @param array $notes Notes data, can be an array of objects.
	 *
	 * @return array
	 */
	public function parse_notes( $notes ): array {
		$parsed_notes = array();

		if ( ! is_array( $notes ) || empty( $notes ) ) {
			return $parsed_notes;
		}

		foreach ( $notes as $note ) {
			if ( is_object( $note ) ) {
				$note = (array) $note;
			}

			if ( ! isset( $note['type'], $note['text'] ) ) {
				continue; // Skip if note is not valid.
			}

			$parsed_notes[] = array(
				'type' => $note['type'],
				'text' => $note['text'],
			);
		}

		return $parsed_notes;
	}

	/**
	 * Parse address information and return a standardized array.
	 *
	 * @param mixed $address Address data, can be an object or array.
	 *
	 * @return array
	 */
	public function parse_address_info( $address ): array {
		$address         = json_decode( wp_json_encode( $address ), true );
		$default_address = array(
			'name'                  => '',
			'company'               => '',
			'phone'                 => '',
			'address_line_1'        => '',
			'address_line_2'        => '',
			'address_line_3'        => '',
			'city'                  => '',
			'state_province'        => '',
			'postal_code'           => '',
			'country_code'          => '',
			'is_verified'           => false,
			'residential_indicator' => '',
			'pickup_location'       => array(
				'carrier_id' => '',
				'relay_id'   => '',
			),
		);

		return wp_parse_args( $address, $default_address );
	}
}
