<?php
/**
 * Order Save — transfer WC session locker data to order meta on checkout submit.
 *
 * Supports both Classic Checkout and Blocks Checkout.
 *
 * @package ParcelSelector
 */

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

// Classic checkout (WC 2.x – 9.x)
add_action( 'woocommerce_checkout_update_order_meta', 'parcselect_save_order_meta_classic' );

// Blocks checkout: legacy hook (WC 6.x–8.3)
add_action( 'woocommerce_store_api_checkout_update_order_meta', 'parcselect_save_order_meta_classic' );

// Blocks checkout: new hook (WC 8.4+) — fired after order is created and payment processed
add_action( 'woocommerce_store_api_checkout_order_processed', 'parcselect_save_order_meta_classic' );

// Shipping item label — attaches locker name to the shipping line item
add_action( 'woocommerce_checkout_create_order_shipping_item', 'parcselect_save_shipping_item_meta', 10, 4 );

/**
 * Copy locker data from WC session to order meta.
 *
 * @param int|WC_Order $order_or_id Order ID (classic) or WC_Order object (blocks).
 */
if ( ! function_exists( 'parcselect_save_order_meta_classic' ) ) {
function parcselect_save_order_meta_classic( $order_or_id ) {
	if ( ! function_exists( 'WC' ) || ! WC()->session ) {
		return;
	}

	$order = is_a( $order_or_id, 'WC_Order' ) ? $order_or_id : wc_get_order( $order_or_id );
	if ( ! $order ) {
		return;
	}

	// Prevent duplicate saves when multiple checkout hooks fire for the same
	// order in ONE request (Classic + Blocks Store API both bind here).
	// FIX 2026-05-17 v2: request-scoped static lock instead of a 30s transient.
	// A transient could outlive the request and block a legitimate retry
	// (payment failure -> customer re-submits and changes the locker). A static
	// array is cleared when the request ends, so it dedupes the in-request
	// double-fire without ever blocking a later genuine save. The lock is
	// recorded only AFTER a confirmed save (see $order->save() below).
	$order_id = $order->get_id();
	static $ps_saved_orders = array();
	if ( isset( $ps_saved_orders[ $order_id ] ) ) {
		parcselect_log( 'Order #' . $order_id . ' duplicate save prevented (request-scoped lock)' );
		return;
	}

	$name         = WC()->session->get( 'parcselect_locker_name',         '' );
	$address      = WC()->session->get( 'parcselect_locker_address',      '' );
	$city         = WC()->session->get( 'parcselect_locker_city',         '' );
	$post_code    = WC()->session->get( 'parcselect_locker_post_code',    '' );
	$country      = WC()->session->get( 'parcselect_locker_country',      '' );
	$country_name = WC()->session->get( 'parcselect_locker_country_name', '' );
	$device_type  = WC()->session->get( 'parcselect_device_type',         '' );

	if ( empty( $name ) ) {
		return;
	}

	// BUG-K/L/M/N 2026-05-15: mapa czasem wysyla location_description (np.
	// "Na myjni bezdotykowej", "Przy wejsciu") jako city i pomija post_code.
	// Address1 zawiera poprawny format "Ulica NR, KOD MIASTO" -> parsujemy
	// i nadpisujemy gdy city wyglada na opis lokalu lub postcode pusty/zly.
	if ( '' !== $address && class_exists( 'Parcselect_Label_Embed' )
	     && method_exists( 'Parcselect_Label_Embed', 'parse_locker_address' ) ) {
		$_parsed = Parcselect_Label_Embed::parse_locker_address( $address );
		$_pc_bad   = ( '' === $post_code || ! preg_match( '/^\\d{2}-\\d{3}$/', $post_code ) );
		$_city_bad = ( '' === $city || preg_match( '/^(przy |na |obok|naprzeciw|wejsc|wej\xc5\x9bc|pawilon|myjni|parking|sklep|stacj)/iu', $city ) );
		if ( $_pc_bad && ! empty( $_parsed['postcode'] ) ) {
			$post_code = $_parsed['postcode'];
		}
		if ( $_city_bad && ! empty( $_parsed['city'] ) ) {
			$city = $_parsed['city'];
		}
		parcselect_log( 'Order locker parse: city=' . $city . ' post_code=' . $post_code . ' (parsed from address1)' );
	}

	// Verify that the order shipping method is ours
	$chosen = $order->get_shipping_methods();
	$is_ps = false;
	foreach ( $chosen as $method ) {
		if ( false !== strpos( $method->get_method_id(), 'parcselect_inpost_paczkomat' ) ) {
			$is_ps = true;
			break;
		}
	}

	if ( ! $is_ps ) {
		return;
	}

	// Primary meta keys
	$order->update_meta_data( '_ps_locker_name',         $name );
	$order->update_meta_data( '_ps_locker_address1',     $address );
	$order->update_meta_data( '_ps_locker_city',         $city );
	$order->update_meta_data( '_ps_locker_post_code',    $post_code );
	$order->update_meta_data( '_ps_locker_country',      $country );
	$order->update_meta_data( '_ps_locker_country_name', $country_name );
	$order->update_meta_data( '_ps_device_type',         $device_type );

	// Carrier identification (InPost Paczkomaty).
	$carrier = WC()->session->get( 'parcselect_carrier', '' );
	if ( ! empty( $carrier ) ) {
		$order->update_meta_data( '_ps_carrier', $carrier );
	}

	// Flag: was this a custom pickup point (free, no carrier)?
	$is_custom = WC()->session->get( 'parcselect_is_custom_pickup', '0' );
	if ( '1' === $is_custom ) {
		$order->update_meta_data( '_ps_is_custom_pickup', '1' );
	}

	// DEV-219-K1 2026-05-16: synchronizuj pozycje wysylki z aktualnym paczkomatem.
	// Pozycja wysylki bywa tworzona (hook woocommerce_checkout_create_order_shipping_item)
	// z wczesniejsza wartoscia sesji niz finalny wybor -> zamowienie pokazywalo DWA
	// rozne paczkomaty (stary w pozycji wysylki, nowy w _ps_locker_name). Ta funkcja
	// dziala PO utworzeniu pozycji, wiec ja koryguje. Zasada: ostatni wybrany
	// paczkomat jest jedynym wlasciwym -> pozycja wysylki podaza za _ps_locker_name.
	$_k1_label = ( '1' === (string) $is_custom )
		? __( 'Pickup Point', 'parcel-selector' )
		: __( 'Parcel Locker', 'parcel-selector' );
	// Przeciwny klucz — gdyby pozycja powstala z innym typem punktu (custom <-> paczkomat),
	// usuwamy go, by zamowienie nie mialo dwoch rownoleglych meta paczkomatu.
	$_k1_other = ( '1' === (string) $is_custom )
		? __( 'Parcel Locker', 'parcel-selector' )
		: __( 'Pickup Point', 'parcel-selector' );
	foreach ( $order->get_items( 'shipping' ) as $_k1_item ) {
		if ( false === strpos( (string) $_k1_item->get_method_id(), 'parcselect_inpost_paczkomat' ) ) {
			continue;
		}
		$_k1_dirty = false;
		if ( (string) $_k1_item->get_meta( $_k1_label ) !== (string) $name ) {
			$_k1_item->update_meta_data( $_k1_label, $name );
			$_k1_dirty = true;
		}
		if ( $_k1_label !== $_k1_other && '' !== (string) $_k1_item->get_meta( $_k1_other ) ) {
			$_k1_item->delete_meta_data( $_k1_other );
			$_k1_dirty = true;
		}
		// FIX 2026-05-17: also drop literal-English locker keys left over when the
		// shipping item was created under a different admin/checkout locale, so the
		// item never carries two parallel locker meta keys.
		foreach ( array( 'Parcel Locker', 'Pickup Point' ) as $_k1_stale ) {
			if ( $_k1_stale !== $_k1_label && '' !== (string) $_k1_item->get_meta( $_k1_stale ) ) {
				$_k1_item->delete_meta_data( $_k1_stale );
				$_k1_dirty = true;
			}
		}
		$_k1_base = preg_replace( '/\s*\([^()]*\)\s*$/', '', (string) $_k1_item->get_name() );
		$_k1_new  = ( '' !== $_k1_base ) ? $_k1_base . ' (' . $name . ')' : $_k1_item->get_name();
		if ( $_k1_item->get_name() !== $_k1_new ) {
			$_k1_item->set_name( $_k1_new );
			$_k1_dirty = true;
		}
		if ( $_k1_dirty ) {
			$_k1_item->save();
		}
	}

	// Customer data (from popup form, if available)
	$cust_fn = WC()->session->get( 'parcselect_cust_first_name', '' );
	$cust_ln = WC()->session->get( 'parcselect_cust_last_name',  '' );
	$cust_em = WC()->session->get( 'parcselect_cust_email',      '' );
	$cust_ph = WC()->session->get( 'parcselect_cust_phone',      '' );

	if ( ! empty( $cust_fn ) ) {
		$order->update_meta_data( '_ps_customer_first_name', $cust_fn );
		$order->update_meta_data( '_ps_customer_last_name',  $cust_ln );
		$order->update_meta_data( '_ps_customer_email',      $cust_em );
		$order->update_meta_data( '_ps_customer_phone',      $cust_ph );
	}

	// Update WC shipping address to reflect locker location
	// We only update IF we have non-empty data to avoid clearing customer's address if session is partially lost.
	if ( ! empty( $address ) ) {
		$order->set_shipping_address_1( $address );
		if ( ! empty( $city ) ) {
			$order->set_shipping_city( $city );
		}
		if ( ! empty( $post_code ) ) {
			$order->set_shipping_postcode( $post_code );
		}
		if ( ! empty( $country ) ) {
			$order->set_shipping_country( strtoupper( $country ) );
		}
	}

	// ⏱️ Save performance metrics (selection time and total time)
	$s_time = (int) WC()->session->get( 'parcselect_selection_time', 0 );
	$t_time = (int) WC()->session->get( 'parcselect_total_time',     0 );

	if ( $s_time > 0 || $t_time > 0 ) {
		$metrics_str = sprintf( 'Total: %ds | Selection: %ds', $t_time, $s_time );
		$order->update_meta_data( '_ps_time_to_order', $metrics_str );
	}

	// Delivery time + order note: instance settings take priority, global admin panel config is fallback
	$global_cfg    = get_option( 'parcselect_shipping_config', array() );
	$delivery_time = isset( $global_cfg['delivery_time'] ) ? sanitize_text_field( $global_cfg['delivery_time'] ) : '';
	$order_note    = isset( $global_cfg['order_note'] )    ? sanitize_textarea_field( $global_cfg['order_note'] ) : '';

	$shipping_items = $order->get_shipping_methods();
	foreach ( $shipping_items as $item ) {
		if ( false !== strpos( $item->get_method_id(), 'parcselect_inpost_paczkomat' ) ) {
			$instance_id    = $item->get_instance_id();
			$opts           = get_option( 'woocommerce_parcselect_inpost_paczkomat_' . $instance_id . '_settings', array() );
			$inst_dt        = isset( $opts['delivery_time'] ) ? sanitize_text_field( $opts['delivery_time'] ) : '';
			$inst_note      = isset( $opts['order_note'] )    ? sanitize_textarea_field( $opts['order_note'] ) : '';
			// Instance overrides global when non-empty
			if ( ! empty( $inst_dt ) ) $delivery_time = $inst_dt;
			if ( ! empty( $inst_note ) ) $order_note   = $inst_note;
			break;
		}
	}

	if ( ! empty( $delivery_time ) ) {
		$order->update_meta_data( '_ps_delivery_time', $delivery_time );
	}
	if ( ! empty( $order_note ) ) {
		$order->update_meta_data( '_ps_order_note', $order_note );
	}

	/**
	 * Fires just before the PS order meta is persisted.
	 *
	 * @param WC_Order $order Order object with all PS meta already set.
	 */
	do_action( 'parcselect_before_order_save', $order );

	try {
		$order->save();
		// FIX 2026-05-17 v2: record the request-scoped lock only AFTER a confirmed
		// save so the other checkout hook in this request skips; a run that bailed
		// earlier never reaches here so never locks.
		$ps_saved_orders[ $order_id ] = true;
		// In HPOS mode (WC 7.1+), explicitly clear the order cache after save
		// to ensure admin views show fresh data immediately.
		if ( class_exists( 'Automattic\WooCommerce\Utilities\OrderCacheController' ) ) {
			wc_get_container()->get( 'Automattic\WooCommerce\Utilities\OrderCacheController' )->flush_order_caches( array( $order->get_id() ) );
		}
	} catch ( \Exception $e ) {
		parcselect_log( 'Order #' . $order->get_id() . ' save FAILED: ' . $e->getMessage() );
		// Log failure as order note so admin is aware. Do not re-throw —
		// breaking checkout is worse than missing meta (meta is still in memory).
		try {
			$order->add_order_note(
				/* translators: %s: error message */
				sprintf( __( 'ParcelSelector: locker data save failed — %s. Please verify shipping details.', 'parcel-selector' ), $e->getMessage() )
			);
		} catch ( \Exception $e2 ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
			// If even note fails, we already logged above.
		}
	}

	/**
	 * Fires after the PS order meta has been saved.
	 *
	 * @param WC_Order $order  Saved order.
	 * @param string   $name   Selected locker ID.
	 * @param string   $city   Locker city.
	 */
	do_action( 'parcselect_after_order_save', $order, $name, $city );

	parcselect_log( 'Order meta saved: order=' . $order->get_id() . ' locker=' . $name . ' city=' . $city );
}
}

/**
 * Add locker name to shipping item meta (shows in WC order details).
 *
 * @param WC_Order_Item_Shipping $item     Shipping item.
 * @param string                 $package_key Package key.
 * @param array                  $package     Package data.
 * @param WC_Order               $order       Order object.
 */
if ( ! function_exists( 'parcselect_save_shipping_item_meta' ) ) {
function parcselect_save_shipping_item_meta( $item, $package_key, $package, $order ) {
	// Only for our shipping method — check early to avoid unnecessary session access
	if ( false === strpos( $item->get_method_id(), 'parcselect_inpost_paczkomat' ) ) {
		return;
	}

	// Try to get locker from session first, then fall back to order meta
	$locker     = '';
	$is_custom  = '0';

	if ( function_exists( 'WC' ) && WC()->session ) {
		$locker    = (string) WC()->session->get( 'parcselect_locker_name', '' );
		$is_custom = (string) WC()->session->get( 'parcselect_is_custom_pickup', '0' );
	}

	// Fallback: read from order object (e.g. REST API checkout without session)
	if ( empty( $locker ) && is_a( $order, 'WC_Order' ) ) {
		$locker    = (string) $order->get_meta( '_ps_locker_name' );
		$is_custom = (string) $order->get_meta( '_ps_is_custom_pickup' );
	}

	if ( empty( $locker ) ) {
		return;
	}

	$label = '1' === $is_custom
		? __( 'Pickup Point', 'parcel-selector' )
		: __( 'Parcel Locker', 'parcel-selector' );
	$item->update_meta_data( $label, $locker );
}
}
