<?php

namespace Simplyin\Simplyin_W_Plugin\Dto\Order;

use WC_Order;
use WC_Order_Item;

class Order_DTO_Factory {
	/**
	 * @param WC_Order_Item $item
	 * @param WC_Order $order
	 *
	 * @return Order_Item_DTO
	 */
	public static function create_order_item(
		WC_Order_Item $item,
		WC_Order $order
	): Order_Item_DTO {
		$simplyin_order_item = new Order_Item_DTO();
		$product_id          = $item->get_product_id();
		$product             = wc_get_product( $product_id ); // Parent product (taxonomy / attributes).
		$line_product        = $item instanceof \WC_Order_Item_Product ? $item->get_product() : $product; // Variation-aware (sku / ean).

		$taxes       = $item->get_taxes();
		$quantity    = $item->get_quantity() ?? 1;
		$line_tax    = (float) array_sum( $taxes['total'] );
		$line_net    = (float) $item->get_total();
		$line_amounts = self::compute_line_tax( $line_tax, $line_net, $quantity );
		$tax_amount  = $line_amounts['tax_amount'];
		$tax_rate    = $line_amounts['tax_rate'];
		$price_net   = self::normalize_price( $order->get_item_total( $item ) );
		$price_total = self::normalize_price( $price_net + $tax_amount );

		// Gross line total (all units, after discount): the money field the backend reads.
		$line_total = self::round_price( (float) $item->get_total() + (float) $item->get_total_tax() );

		$simplyin_order_item->set_name( $item->get_name() );
		$simplyin_order_item->set_url( get_permalink( $product_id ) );
		$simplyin_order_item->set_price( $price_total );
		$simplyin_order_item->set_price_net( $price_net );
		$simplyin_order_item->set_total_amount( $line_total );
		$simplyin_order_item->set_quantity( $quantity );
		$simplyin_order_item->set_tax_amount( $tax_amount );
		$simplyin_order_item->set_tax_rate( $tax_rate );
		$simplyin_order_item->set_thumbnail_url(
			$product instanceof \WC_Product
				? self::get_product_thumbnail_url( $item, $product )
				: ''
		);
		$simplyin_order_item->set_currency( $order->get_currency() );

		// Broad product data (all optional; only set when present, never blocks the order).
		// Identifiers come from the line product (the variation when applicable), with a parent fallback.
		$sku = $line_product instanceof \WC_Product ? trim( (string) $line_product->get_sku() ) : '';
		if ( '' === $sku && $product instanceof \WC_Product ) {
			$sku = trim( (string) $product->get_sku() );
		}
		if ( '' !== $sku ) {
			$simplyin_order_item->set_sku( $sku );
		}

		$ean = $line_product instanceof \WC_Product ? self::extract_product_ean( $line_product ) : '';
		if ( '' === $ean && $product instanceof \WC_Product ) {
			$ean = self::extract_product_ean( $product );
		}
		if ( '' !== $ean ) {
			$simplyin_order_item->set_ean( $ean );
		}

		// Brand lives on the parent product taxonomy.
		if ( $product instanceof \WC_Product ) {
			$brand = self::get_first_term_name( $product->get_id(), [ 'product_brand', 'pwb-brand', 'pa_brand' ] );
			if ( '' !== $brand ) {
				$simplyin_order_item->set_brand( $brand );
			}
		}

		$attributes = self::build_product_attributes( $item, $product );
		if ( ! empty( $attributes ) ) {
			$simplyin_order_item->set_attributes( $attributes );
		}

		return $simplyin_order_item;


	}

	/**
	 * Per-unit tax amount and the effective tax rate (%) for a line item.
	 *
	 * The rate is derived from the whole-line values (line tax / line net), which keeps it
	 * correct at any quantity; deriving it from the per-unit tax against the line net total
	 * understates the rate by a factor of the quantity.
	 *
	 * @param float $line_tax Line total tax (all units).
	 * @param float $line_net Line total net (all units, after discount).
	 * @param int   $quantity Line quantity.
	 *
	 * @return array{tax_amount: float, tax_rate: float}
	 */
	public static function compute_line_tax( float $line_tax, float $line_net, int $quantity ): array {
		$per_unit_tax = $quantity > 0 ? $line_tax / $quantity : $line_tax;

		return [
			'tax_amount' => self::normalize_price( $per_unit_tax ),
			'tax_rate'   => self::normalize_price( $line_net > 0 ? ( $line_tax / $line_net ) * 100 : 0 ),
		];
	}

	private const MAX_ITEM_ATTRIBUTES        = 30;
	private const MAX_ATTRIBUTE_VALUE_LENGTH = 500;

	/**
	 * Collects a broad set of product attributes as { name, value } pairs. Every source is
	 * best-effort and wrapped so a missing/broken taxonomy never blocks order creation. Raw
	 * values are sent as-is; the backend normalizes known keys (aliases, `pa_` prefix) and
	 * accepts unknown ones.
	 *
	 * @param WC_Order_Item        $item
	 * @param \WC_Product|false|null $product Parent product.
	 *
	 * @return array<int, array{name: string, value: string}>
	 */
	private static function build_product_attributes( WC_Order_Item $item, $product ): array {
		$pairs = [];

		// Curated, high-value fields first so that when the 30-entry cap trims, it drops only
		// surplus open attributes (appended last), never the chosen variation / category /
		// description data.

		// Chosen variation selections (e.g. Color: Red) for variable products.
		if ( $item instanceof \WC_Order_Item_Product ) {
			$pairs = array_merge( $pairs, self::get_variation_attribute_pairs( $item ) );
		}

		if ( $product instanceof \WC_Product ) {
			$product_id = $product->get_id();

			$pairs[] = [ 'name' => 'category', 'value' => self::get_category_path( $product_id ) ];
			$pairs[] = [ 'name' => 'tags', 'value' => self::get_term_names( $product_id, 'product_tag' ) ];
			$pairs[] = [ 'name' => 'weight', 'value' => (string) $product->get_weight() ];
			$pairs[] = [ 'name' => 'length', 'value' => (string) $product->get_length() ];
			$pairs[] = [ 'name' => 'width', 'value' => (string) $product->get_width() ];
			$pairs[] = [ 'name' => 'height', 'value' => (string) $product->get_height() ];
			$pairs[] = [ 'name' => 'short_description', 'value' => wp_strip_all_tags( $product->get_short_description() ) ];
			$pairs[] = [ 'name' => 'description', 'value' => wp_strip_all_tags( $product->get_description() ) ];

			// Open-ended global product attributes (material, gender, …) last: these are the
			// entries the 30-cap sheds first when a product carries an unusually large set.
			$pairs = array_merge( $pairs, self::get_product_attribute_pairs( $product ) );
		}

		return self::normalize_attribute_pairs( $pairs );
	}

	/**
	 * Normalizes the collected pairs: trims, drops blank name/value, multibyte-safe truncates
	 * long values, dedupes by (case-insensitive) name — first wins — and caps the count.
	 *
	 * @param array<int, array{name: string, value: mixed}> $pairs
	 *
	 * @return array<int, array{name: string, value: string}>
	 */
	private static function normalize_attribute_pairs( array $pairs ): array {
		$seen   = [];
		$result = [];

		foreach ( $pairs as $pair ) {
			$name  = trim( (string) $pair['name'] );
			$value = is_array( $pair['value'] ) ? implode( ', ', $pair['value'] ) : trim( (string) $pair['value'] );
			if ( '' === $name || '' === $value ) {
				continue; // Omit empty entries entirely — never send a blank pair.
			}

			// Multibyte-safe cap so UTF-8 (e.g. Polish) text is not cut mid-character.
			$length = function_exists( 'mb_strlen' ) ? mb_strlen( $value ) : strlen( $value );
			if ( $length > self::MAX_ATTRIBUTE_VALUE_LENGTH ) {
				$value = function_exists( 'mb_substr' )
					? mb_substr( $value, 0, self::MAX_ATTRIBUTE_VALUE_LENGTH )
					: substr( $value, 0, self::MAX_ATTRIBUTE_VALUE_LENGTH );
			}

			$key = function_exists( 'mb_strtolower' ) ? mb_strtolower( $name ) : strtolower( $name );
			if ( isset( $seen[ $key ] ) ) {
				continue;
			}
			$seen[ $key ] = true;
			$result[]     = [ 'name' => $name, 'value' => $value ];

			if ( count( $result ) >= self::MAX_ITEM_ATTRIBUTES ) {
				break;
			}
		}

		return $result;
	}

	/**
	 * WooCommerce 9.2+ native GTIN, with common meta fallbacks for older / custom setups.
	 */
	private static function extract_product_ean( \WC_Product $product ): string {
		if ( method_exists( $product, 'get_global_unique_id' ) ) {
			$gtin = trim( (string) $product->get_global_unique_id() );
			if ( '' !== $gtin ) {
				return $gtin;
			}
		}
		foreach ( [ '_ean', 'pa_ean', '_gtin', '_upc', '_barcode', 'ean', 'gtin' ] as $meta_key ) {
			$raw = $product->get_meta( $meta_key );
			if ( ! is_scalar( $raw ) ) {
				continue; // Some plugins store meta as arrays; skip non-scalar values.
			}
			$value = trim( (string) $raw );
			if ( '' !== $value ) {
				return $value;
			}
		}

		return '';
	}

	/**
	 * Deepest product-category path, e.g. "Men > Shirts > Casual".
	 */
	private static function get_category_path( int $product_id ): string {
		$terms = get_the_terms( $product_id, 'product_cat' );
		if ( ! is_array( $terms ) || empty( $terms ) ) {
			return '';
		}

		$best_path  = [];
		$best_depth = -1;
		foreach ( $terms as $term ) {
			$ancestors = get_ancestors( $term->term_id, 'product_cat' );
			$depth     = count( $ancestors );
			if ( $depth <= $best_depth ) {
				continue;
			}
			$best_depth = $depth;
			$path       = [ $term->name ];
			foreach ( $ancestors as $ancestor_id ) {
				$ancestor = get_term( $ancestor_id, 'product_cat' );
				if ( $ancestor instanceof \WP_Term ) {
					array_unshift( $path, $ancestor->name );
				}
			}
			$best_path = $path;
		}

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

	/**
	 * @return string Comma-separated term names for the given taxonomy.
	 */
	private static function get_term_names( int $product_id, string $taxonomy ): string {
		$names = wp_get_post_terms( $product_id, $taxonomy, [ 'fields' => 'names' ] );
		if ( is_wp_error( $names ) || ! is_array( $names ) ) {
			return '';
		}

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

	/**
	 * First non-empty term name across the given taxonomies (e.g. brand variants).
	 *
	 * @param array<int, string> $taxonomies
	 */
	private static function get_first_term_name( int $product_id, array $taxonomies ): string {
		foreach ( $taxonomies as $taxonomy ) {
			if ( ! taxonomy_exists( $taxonomy ) ) {
				continue;
			}
			$names = wp_get_post_terms( $product_id, $taxonomy, [ 'fields' => 'names' ] );
			if ( ! is_wp_error( $names ) && ! empty( $names ) ) {
				return (string) $names[0];
			}
		}

		return '';
	}

	/**
	 * Chosen variation attribute selections for the line item (e.g. Color: Red), resolved to
	 * human labels/values — not arbitrary line-item meta.
	 *
	 * @return array<int, array{name: string, value: string}>
	 */
	private static function get_variation_attribute_pairs( \WC_Order_Item_Product $item ): array {
		$variation = $item->get_product();
		if ( ! $variation instanceof \WC_Product_Variation ) {
			return [];
		}

		$pairs = [];
		foreach ( $variation->get_variation_attributes() as $attribute_key => $attribute_value ) {
			$attribute_value = (string) $attribute_value;
			if ( '' === $attribute_value ) {
				continue; // "Any …" variations leave the value blank.
			}
			$taxonomy = str_replace( 'attribute_', '', (string) $attribute_key );
			$label    = wc_attribute_label( $taxonomy, $variation );
			$value    = $attribute_value;
			if ( taxonomy_exists( $taxonomy ) ) {
				$term = get_term_by( 'slug', $attribute_value, $taxonomy );
				if ( $term instanceof \WP_Term ) {
					$value = $term->name;
				}
			}
			$pairs[] = [
				'name'  => (string) $label,
				'value' => $value,
			];
		}

		return $pairs;
	}

	/**
	 * Global product attributes (taxonomy-based and custom) as labelled name/value pairs.
	 *
	 * @return array<int, array{name: string, value: string}>
	 */
	private static function get_product_attribute_pairs( \WC_Product $product ): array {
		$pairs = [];
		foreach ( $product->get_attributes() as $attribute ) {
			if ( ! $attribute instanceof \WC_Product_Attribute ) {
				continue;
			}
			$label = wc_attribute_label( $attribute->get_name(), $product );
			if ( $attribute->is_taxonomy() ) {
				$values = wc_get_product_terms( $product->get_id(), $attribute->get_name(), [ 'fields' => 'names' ] );
				$value  = is_array( $values ) ? implode( ', ', $values ) : '';
			} else {
				$value = implode( ', ', $attribute->get_options() );
			}
			$pairs[] = [
				'name'  => (string) $label,
				'value' => (string) $value,
			];
		}

		return $pairs;
	}

	public static function build_create_order_body_data_dto(
		WC_Order $order,
		array $payment_method_data,
		string $plugin_version,
		string $woocommerce_version,
		$tax_id = '',
		string $language_code = '',
		string $parcel_machine_id = '',
		string $parcel_provider_name = '',
		string $phone_app_input_field = '',
		string $simply_billing_id = '',
		string $simply_shipping_id = '',
		string $shop_user_email = ''

	): Order_Body_Data_DTO {

		if ( '' === $shop_user_email ) {
			$shop_user_email = $order->get_billing_email() ?? '';
		}

		$items_data_dto = self::create_order_items( $order );
		$order_data_dto = self::create_order_data_dto( $order,
			$items_data_dto,
			$payment_method_data,
			$plugin_version,
			$woocommerce_version,
			$shop_user_email,
			$tax_id,
			$simply_billing_id,
			false
		);

		// Always include shippingData, prefer parcel point address when available
		$shipping_data_dto = self::create_shipping_data_dto( $order, $simply_shipping_id );
		self::override_shipping_from_session_parcel( $shipping_data_dto );
		$order_data_dto->set_shipping_data_dto( $shipping_data_dto );

		if ( $parcel_machine_id !== '' && $parcel_provider_name !== '' ) {
			$parcel_locker_minimal_info_dto = self::create_parcel_locker_minimal_info_dto(
				$parcel_machine_id,
				$parcel_provider_name
			);
			$order_data_dto->set_parcel_locker_minimal_info_DTO( $parcel_locker_minimal_info_dto );
		}

		$order_body_data_dto = new Order_Body_Data_DTO();
		$order_body_data_dto->set_order_data_dto( $order_data_dto );
		$order_body_data_dto->set_shop_user_email( $shop_user_email );

		return $order_body_data_dto;
	}

	public static function build_create_order_without_account_body_data_dto(
		WC_Order $order,
		array $payment_method_data,
		string $plugin_version,
		string $woocommerce_version,
		$tax_id = '',
		string $language_code = '',
		string $parcel_machine_id = '',
		string $parcel_provider_name = '',
		string $phone_app_input_field = '',
		string $simply_billing_id = '',
		string $simply_shipping_id = '',
		string $shop_user_email = ''

	): Order_Without_Account_Body_Data_DTO {

		// If e-mail not provided, prefer address used in the current order. This keeps the
		// shopUserEmail consistent with the user represented by the api_token.
		if ( '' === $shop_user_email ) {
			$shop_user_email = $order->get_billing_email() ?: ( wp_get_current_user()->data->user_email ?? '' );
		}

		$items_data_dto = self::create_order_items( $order );
		$order_data_dto = self::create_order_data_dto( $order,
			$items_data_dto,
			$payment_method_data,
			$plugin_version,
			$woocommerce_version,
			$shop_user_email,
			$tax_id,
			$simply_billing_id,
			false
		);

		// Always include shippingData
		$shipping_data_dto = self::create_shipping_data_dto( $order, $simply_shipping_id );
		self::override_shipping_from_session_parcel( $shipping_data_dto );
		$order_data_dto->set_shipping_data_dto( $shipping_data_dto );

		if ( $parcel_machine_id !== '' && $parcel_provider_name !== '' ) {
			$parcel_locker_minimal_info_dto = self::create_parcel_locker_minimal_info_dto(
				$parcel_machine_id,
				$parcel_provider_name
			);
			$order_data_dto->set_parcel_locker_minimal_info_DTO( $parcel_locker_minimal_info_dto );
		}

		$order_without_account_body_data_dto = new Order_Without_Account_Body_Data_DTO();
		$order_without_account_body_data_dto->set_order_data_dto( $order_data_dto );

		return $order_without_account_body_data_dto;
	}

	public static function build_create_order_and_account_body_data_dto(
		WC_Order $order,
		array $payment_method_data,
		string $plugin_version,
		string $woocommerce_version,
		$tax_id = '',
		string $language_code = '',
		string $parcel_machine_id = '',
		string $parcel_provider_name = '',
		string $phone_app_input_field = '',
		string $simply_billing_id = '',
		string $simply_shipping_id = '',
		string $shop_user_email = ''

	): Order_And_Account_Body_Data_DTO {

		// Keep shopUserEmail aligned with e-mail address used for the new account / order.
		if ( '' === $shop_user_email ) {
			$shop_user_email = $order->get_billing_email() ?: ( wp_get_current_user()->data->user_email ?? '' );
		}

		$items_data_dto = self::create_order_items( $order );


		// Use the order number as unique ID
		$unique_id = $order->get_order_number() . '-' . uniqid();

		$new_account_data_dto = self::create_new_account_data_dto(
			trim( $order->get_billing_first_name() ),
			trim( $order->get_billing_last_name() ),
			trim( $phone_app_input_field ),
			trim( $order->get_billing_email() ),
			$unique_id,
			$language_code,
			false );


		$order_data_dto = self::create_order_data_dto( $order,
			$items_data_dto,
			$payment_method_data,
			$plugin_version,
			$woocommerce_version,
			$shop_user_email,
			$tax_id,
			$simply_billing_id,
			true
		);

		// Always include shippingData
		$shipping_data_dto = self::create_shipping_data_dto( $order, $simply_shipping_id );
		self::override_shipping_from_session_parcel( $shipping_data_dto );
		$order_data_dto->set_shipping_data_dto( $shipping_data_dto );

		if ( $parcel_machine_id !== '' && $parcel_provider_name !== '' ) {
			$parcel_locker_minimal_info_dto = self::create_parcel_locker_minimal_info_dto(
				$parcel_machine_id,
				$parcel_provider_name
			);
			$order_data_dto->set_parcel_locker_minimal_info_DTO( $parcel_locker_minimal_info_dto );
		}

		$order_and_account_body_data_dto = new Order_And_Account_Body_Data_DTO();
		$order_and_account_body_data_dto->set_new_account_data_dto( $new_account_data_dto );
		$order_and_account_body_data_dto->set_order_data_dto( $order_data_dto );


		return $order_and_account_body_data_dto;
	}

	public static function create_order_data_dto(
		WC_Order $order,
		Order_Items_DTO $items_data_dto,
		$payment_method_data,
		$plugin_version,
		$woocommerce_version,
		string $shop_user_email,
		$taxId = null,
		string $simply_billing_id = '',
		bool $placed_during_account_creation = false,
		?Parcel_Locker_Data_DTO $parcel_locker_data_DTO = null
	): Order_Data_DTO {

		$host_addres = gethostname();
		$host_ip     = gethostbyname( $host_addres );

		$order_data_dto = new Order_Data_DTO();
		$order_data_dto->set_payment_method( $payment_method_data['method'] );
		$order_data_dto->set_payment_method_title( $payment_method_data['title'] );
		$order_data_dto->set_shop_order_number( $order->get_order_number() );

		// Financial breakdown (B2C = gross): the authoritative money model. The
		// legacy price/shippingPrice fields are derived from the same values and kept for
		// backward compatibility with older backends.
		$financials = self::compute_order_financials( $order );
		$order_data_dto->set_price( self::normalize_price( $order->get_total() ) );
		$order_data_dto->set_shipping_price( $financials['shipping_amount'] );
		$order_data_dto->set_total_amount( $financials['total_amount'] );
		$order_data_dto->set_subtotal_amount( $financials['subtotal_amount'] );
		$order_data_dto->set_discount_amount( $financials['discount_amount'] );
		$order_data_dto->set_shipping_amount( $financials['shipping_amount'] );
		$order_data_dto->set_tax_amount( $financials['tax_amount'] );

		$order_data_dto->set_currency( $order->get_currency() );
		$order_data_dto->set_items( $items_data_dto );
		$order_data_dto->set_placed_during_account_creation( $placed_during_account_creation );
		$order_data_dto->set_billing_data( self::create_billing_data_dto( $order,
			$taxId,
			$simply_billing_id ) );
		$order_data_dto->set_shop_name( addslashes( sanitize_text_field( get_bloginfo( 'name' ) ) ) );
		$order_data_dto->set_plugin_version( $plugin_version );
		$order_data_dto->set_shop_version( $woocommerce_version );
		$order_data_dto->set_shop_user_email( $shop_user_email );
		$order_data_dto->set_user_ip( $order->get_customer_ip_address() );
		$order_data_dto->set_shop_ip( $host_ip );
		$order_data_dto->set_parcel_locker_data_DTO( $parcel_locker_data_DTO );
		$order_data_dto->set_status_info( self::build_status_info( $order ) );

		$date_modified = $order->get_date_modified() ?? $order->get_date_created();
		$order_data_dto->set_updated_at( self::format_api_datetime( $date_modified ) );

		// Original order-placement date, distinct from updatedAt above: backend needs
		// the true purchase date on history import instead of defaulting to the import time.
		$order_data_dto->set_order_date( self::format_api_datetime( $order->get_date_created() ) );

		return $order_data_dto;
	}

	public static function build_status_info( WC_Order $order ): Status_Info_DTO {
		$wc_status   = $order->get_status();
		$date_object = $order->get_date_modified() ?? $order->get_date_created();
		$iso_date    = self::format_api_datetime( $date_object );

		$dto = new Status_Info_DTO();
		$dto->set_order_status( $wc_status );
		$dto->set_custom_order_status( '' );
		$dto->set_shipment_status( '' );
		$dto->set_custom_shipment_status( '' );
		$dto->set_status( self::map_wc_status_to_simplyin( $wc_status ) );
		$dto->set_date( $iso_date );
		$dto->set_city( '' );
		$dto->set_courier_slug( '' );
		$dto->set_location( '' );
		$dto->set_description( '' );

		return $dto;
	}

	public static function create_new_account_data_dto(
		string $name,
		string $surname,
		string $phone_number,
		string $email,
		string $unique_id,
		string $language,
		bool $marketing_consent
	): New_Account_Data_DTO {

		$new_account_data_dto = new New_Account_Data_DTO();
		$new_account_data_dto->set_name( $name );
		$new_account_data_dto->set_surname( $surname );
		$new_account_data_dto->set_phone_number( $phone_number );
		$new_account_data_dto->set_email( $email );
		$new_account_data_dto->set_unique_id( $unique_id );
		$new_account_data_dto->set_language( $language );
		$new_account_data_dto->set_marketing_consent( $marketing_consent );

		return $new_account_data_dto;
	}

	/**
	 * @param WC_Order $order
	 *
	 * @return Order_Items_DTO
	 */
	public static function create_order_items( WC_Order $order
	): Order_Items_DTO {
		$simplyin_order_items = new Order_Items_DTO();
		$items                = $order->get_items();
		foreach ( $items as $wc_order_item ) {
			$simplyin_order_item = self::create_order_item( $wc_order_item,
				$order );
			$simplyin_order_items->push( $simplyin_order_item );
		}

		return $simplyin_order_items;
	}

	public static function create_billing_data_dto(
		WC_Order $order,
		$tax_id = null,
		$simply_billing_id = ''
	): Billing_Data_DTO {
		$billing_data_dto = new Billing_Data_DTO();

		$billing_data_dto->set_name( trim( $order->get_billing_first_name() ) );
		$billing_data_dto->set_surname( trim( $order->get_billing_last_name() ) );
		$billing_data_dto->set_street( trim( $order->get_billing_address_1() ) );
		$billing_data_dto->set_appartment_number( trim( $order->get_billing_address_2() ) );
		$billing_data_dto->set_city( trim( $order->get_billing_city() ) );
		$billing_data_dto->set_postal_code( trim( $order->get_billing_postcode() ) );
		$billing_data_dto->set_country( trim( $order->get_billing_country() ) );
		$billing_data_dto->set_state( trim( $order->get_billing_state() ) );
		$billing_data_dto->set_company_name( trim( $order->get_billing_company() ) );
		$billing_data_dto->set_tax_id( is_string( $tax_id ) ? trim( $tax_id ) : '' );


		if ( ! empty( $simply_billing_id ) ) {
			$billing_data_dto->set_id( $simply_billing_id );
		}

		return $billing_data_dto;
	}

	public static function create_shipping_data_dto(
		WC_Order $order,
		$simply_shipping_id = ''
	): Shipping_Data_DTO {
		$shipping_data_dto = new Shipping_Data_DTO();

		$shipping_data_dto->set_icon( '🏡' );
		$shipping_data_dto->set_address_name( '' );
		$shipping_data_dto->set_name( trim( $order->get_shipping_first_name() ) );
		$shipping_data_dto->set_surname( trim( $order->get_shipping_last_name() ) );
		$shipping_data_dto->set_street( trim( $order->get_shipping_address_1() ) );
		$shipping_data_dto->set_appartment_number( trim( $order->get_shipping_address_2() ) );
		$shipping_data_dto->set_city( trim( $order->get_shipping_city() ) );
		$shipping_data_dto->set_postal_code( trim( $order->get_shipping_postcode() ) );
		$shipping_data_dto->set_country( trim( $order->get_shipping_country() ) );
		$shipping_data_dto->set_state( trim( $order->get_shipping_state() ) );
		$shipping_data_dto->set_company_name( trim( $order->get_shipping_company() ) );

		if ( ! empty( $simply_shipping_id ) ) {
			$shipping_data_dto->set_id( $simply_shipping_id );
		}

		return $shipping_data_dto;
	}

	public static function create_parcel_locker_minimal_info_dto(
		string $parcel_machine_id,
		string $provider_name
	): Parcel_Locker_Minimal_Info_DTO {
		$parcel_locker_minimal_info_DTO = new Parcel_Locker_Minimal_Info_DTO();
		$parcel_locker_minimal_info_DTO->set_locker_id( $parcel_machine_id );
		$parcel_locker_minimal_info_DTO->set_provider_name( $provider_name );

		return $parcel_locker_minimal_info_DTO;
	}

	public static function create_parcel_locker_data_dto(
		string $label,
		string $locker_id,
		string $address,
		?string $id = null,
		?string $provider_id = null,
		?string $service_type = null,
		?string $logo_url = null,
		?string $icon = null,
		?string $address_name = null,
		?string $coordinates_lat = null,
		?string $coordinates_long = null,
		?string $desc = null
	): Parcel_Locker_Data_DTO {
		$parcel_locker_data_DTO = new Parcel_Locker_Data_DTO();
		$parcel_locker_data_DTO->set_label( $label );
		$parcel_locker_data_DTO->set_locker_id( $locker_id );
		$parcel_locker_data_DTO->set_address( $address );
		$parcel_locker_data_DTO->set_id( $id );
		$parcel_locker_data_DTO->set_provider_id( $provider_id );
		$parcel_locker_data_DTO->set_service_type( $service_type );
		$parcel_locker_data_DTO->set_logo_url( $logo_url );
		$parcel_locker_data_DTO->set_icon( $icon );
		$parcel_locker_data_DTO->set_address_name( $address_name );
		$parcel_locker_data_DTO->set_coordinates_lat( $coordinates_lat );
		$parcel_locker_data_DTO->set_coordinates_long( $coordinates_long );
		$parcel_locker_data_DTO->set_desc( $desc );

		return $parcel_locker_data_DTO;
	}

	public static function normalize_price( $price ): float {
		return abs( self::round_price( $price ) );
	}

	/**
	 * Rounds to 2 decimals preserving sign (unlike normalize_price, which returns abs()).
	 * Needed for discount_amount, which is reported as a negative value.
	 *
	 * @param mixed $price
	 */
	public static function round_price( $price ): float {
		return (float) wc_format_decimal( $price, 2 );
	}

	/**
	 * Computes the gross (B2C) financial breakdown for an order.
	 *
	 * Reconciliation invariant: subtotal_amount + discount_amount + shipping_amount == total_amount.
	 * discount_amount is negative and tax-inclusive; subtotal_amount is the pre-discount gross
	 * (line net + line SUBTOTAL tax), so the discount's tax is not double-counted.
	 *
	 * @return array{total_amount: float, subtotal_amount: float, discount_amount: float, shipping_amount: float, tax_amount: float}
	 */
	public static function compute_order_financials( WC_Order $order ): array {
		// Pre-discount line tax (get_subtotal() is pre-discount net; pair it with subtotal tax
		// so the reconciliation holds once the tax-inclusive discount is subtracted).
		$subtotal_tax = 0.0;
		foreach ( $order->get_items() as $line_item ) {
			if ( is_object( $line_item ) && method_exists( $line_item, 'get_subtotal_tax' ) ) {
				$subtotal_tax += (float) $line_item->get_subtotal_tax();
			}
		}

		// Fees (e.g. COD / payment surcharges) count toward get_total() but not toward
		// get_subtotal()/shipping/discount. The money model has no dedicated fee field, so
		// fold their gross into subtotal_amount to keep the reconciliation exact.
		$fee_gross = 0.0;
		foreach ( $order->get_items( 'fee' ) as $fee_item ) {
			if ( ! is_object( $fee_item ) || ! method_exists( $fee_item, 'get_total' ) || ! method_exists( $fee_item, 'get_total_tax' ) ) {
				continue;
			}
			$fee_gross += (float) $fee_item->get_total() + (float) $fee_item->get_total_tax();
		}

		$subtotal_gross = (float) $order->get_subtotal() + $subtotal_tax + $fee_gross;
		$shipping_gross = (float) $order->get_shipping_total() + (float) $order->get_shipping_tax();
		$discount_gross = (float) $order->get_total_discount( false ); // false = tax-inclusive.

		return [
			'total_amount'    => self::round_price( $order->get_total() ),
			'subtotal_amount' => self::round_price( $subtotal_gross ),
			'discount_amount' => self::round_price( -1 * $discount_gross ),
			'shipping_amount' => self::round_price( $shipping_gross ),
			'tax_amount'      => self::round_price( $order->get_total_tax() ),
		];
	}

	private static function override_shipping_from_session_parcel( Shipping_Data_DTO $shipping_data_dto ): void {
		if ( ! function_exists( 'simplyin' ) ) {
			return;
		}
		$session     = simplyin()->get_simplyin_session();
		$parcel_full = $session->get( 'simplyin_parcel_locker_full', null );
		if ( ! is_array( $parcel_full ) || empty( $parcel_full['address'] ) ) {
			return;
		}
		$address_str = (string) $parcel_full['address'];
		$street      = $address_str;
		$city        = '';
		if ( strpos( $address_str, ',' ) !== false ) {
			$parts  = array_map( 'trim', explode( ',', $address_str ) );
			$city   = (string) array_pop( $parts );
			$street = trim( implode( ', ', $parts ) );
		}
		$address_name = (string) ( $parcel_full['addressName'] ?? ( $parcel_full['label'] ?? '' ) );
		$shipping_data_dto->set_address_name( $address_name );
		if ( $street !== '' ) {
			$shipping_data_dto->set_street( $street );
		}
		if ( $city !== '' ) {
			$shipping_data_dto->set_city( $city );
		}
	}

	private static function map_wc_status_to_simplyin( string $wc_status ): string {
		$map = [
			'pending'        => 'new',
			'on-hold'        => 'new',
			'processing'     => 'new',
			'completed'      => 'Delivered',
			'refunded'       => 'Exception',
			'cancelled'      => 'Exception',
			'failed'         => 'Exception',
			'checkout-draft' => 'new',
		];

		return $map[ $wc_status ] ?? 'new';
	}

	private static function format_api_datetime( ?\WC_DateTime $date ): ?string {
		if ( ! $date ) {
			return null;
		}

		return gmdate( 'Y-m-d\TH:i:s\Z', $date->getTimestamp() );
	}

	private static function get_product_thumbnail_url( \WC_Order_Item_Product $item, \WC_Product $product ): string {
		$variation_id = $item->get_variation_id();

		if ( $variation_id > 0 ) {
			$variation = wc_get_product( $variation_id );
			if ( $variation ) {
				$image_id = $variation->get_image_id();
				if ( $image_id ) {
					$thumbnail_url = wp_get_attachment_image_url( $image_id, 'thumbnail' );
					if ( $thumbnail_url ) {
						return $thumbnail_url;
					}
				}
			}
		}

		return wp_get_attachment_image_url( $product->get_image_id(), 'thumbnail' ) ?: "";
	}
}
