<?php
/**
 * ETYKIETOWNIK hooks — tracking, auto-status, cron, validation, guards, columns, logging.
 *
 * @package ParcelSelector
 * @since   3.4.0
 */

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

class Parcselect_Label_Hooks {

	public static function init() {
		// 1. Tracking number to customer email.
		add_action( 'woocommerce_email_after_order_table', array( __CLASS__, 'email_tracking_info' ), 15, 4 );

		// 1b. Tracking number in My Account order view.
		add_action( 'woocommerce_order_details_after_order_table', array( __CLASS__, 'frontend_tracking_info' ), 15, 1 );

		// 3. Cron — expire stuck awaiting_payment sessions after 24h.
		add_action( 'parcselect_label_cleanup_expired', array( __CLASS__, 'cleanup_expired_sessions' ) );
		add_action( 'parcselect_label_retry_failed', array( __CLASS__, 'retry_failed_labels' ) );
		add_action( 'parcselect_label_prune_old_pdfs', array( __CLASS__, 'prune_old_pdf_mirrors' ) );
		add_action( 'wp_loaded', array( __CLASS__, 'schedule_cleanup' ) );

		// R8 (Konstytucja flow platnosc->etykieta) — czerwony alert admina gdy
		// sa zamowienia oplacone bez etykiety > 24h (status refund_pending).
		add_action( 'admin_notices', array( __CLASS__, 'render_refund_pending_notice' ) );

		// 10. Column on orders list.
		add_filter( 'manage_edit-shop_order_columns', array( __CLASS__, 'add_orders_column' ), 20 );
		add_action( 'manage_shop_order_posts_custom_column', array( __CLASS__, 'render_orders_column' ), 10, 2 );

		// HPOS columns.
		add_filter( 'manage_woocommerce_page_wc-orders_columns', array( __CLASS__, 'add_orders_column' ), 20 );
		add_action( 'manage_woocommerce_page_wc-orders_custom_column', array( __CLASS__, 'render_orders_column_hpos' ), 10, 2 );
	}

	// =========================================================================
	// 1. TRACKING IN CUSTOMER EMAILS
	// =========================================================================

	/**
	 * Add tracking number to WooCommerce order emails.
	 *
	 * @param WC_Order $order         Order object.
	 * @param bool     $sent_to_admin Whether email is sent to admin.
	 * @param bool     $plain_text    Whether email is plain text.
	 * @param WC_Email $email         Email object.
	 */
	public static function email_tracking_info( $order, $sent_to_admin, $plain_text, $email ) {
		if ( ! is_a( $order, 'WC_Order' ) ) {
			return;
		}

		// Cross-install leak guard — never email a customer a tracking number
		// generated by a different installation. Critical: this hook fires on
		// every WC order email; without the guard, leaked _ps_label_tracking_number
		// from a previous install would land in customer inboxes.
		if ( class_exists( 'Parcselect_Label_Installation' ) && ! Parcselect_Label_Installation::owns( $order ) ) {
			if ( Parcselect_Label_Installation::has_any_label_meta( $order ) ) {
				Parcselect_Label_Installation::purge_order( $order );
			}
			return;
		}

		$tracking = $order->get_meta( '_ps_label_tracking_number', true );
		if ( empty( $tracking ) ) {
			return;
		}

		// Get carrier-specific tracking URL.
		$carrier     = $order->get_meta( '_ps_label_carrier', true );
		$carrier     = $carrier ? $carrier : 'inpost';
		$tracking_url = class_exists( 'Parcselect_Label_Carrier' )
			? Parcselect_Label_Carrier::get_tracking_url( $carrier, $tracking )
			: 'https://inpost.pl/sledzenie-przesylek?number=' . $tracking;

		if ( $plain_text ) {
			echo "\n--- " . esc_html__( 'Shipment tracking', 'parcel-selector' ) . " ---\n";
			echo esc_html__( 'Tracking number:', 'parcel-selector' ) . ' ' . esc_html( $tracking ) . "\n";
			echo esc_html__( 'Track your parcel at:', 'parcel-selector' ) . ' ' . esc_url( $tracking_url ) . "\n\n";
		} else {
			echo '<div style="margin:16px 0;padding:12px 16px;background:#f0f6fc;border-left:4px solid #2271b1;border-radius:4px;">';
			echo '<h3 style="margin:0 0 6px;font-size:14px;">' . esc_html__( 'Shipment tracking', 'parcel-selector' ) . '</h3>';
			echo '<p style="margin:0;font-size:13px;">';
			echo '<strong>' . esc_html__( 'Tracking number:', 'parcel-selector' ) . '</strong> ' . esc_html( $tracking );
			echo '<br><a href="' . esc_url( $tracking_url ) . '" style="color:#2271b1;">';
			echo esc_html__( 'Track your parcel', 'parcel-selector' ) . ' &rarr;</a>';
			echo '</p></div>';
		}
	}

	// =========================================================================
	// 1b. TRACKING IN MY ACCOUNT
	// =========================================================================

	/**
	 * Show tracking number on frontend order detail page (My Account).
	 *
	 * @param WC_Order $order Order object.
	 */
	public static function frontend_tracking_info( $order ) {
		if ( ! is_a( $order, 'WC_Order' ) ) {
			return;
		}

		// Cross-install leak guard — never show a customer a tracking number
		// from a different installation in their My Account → Order Details.
		if ( class_exists( 'Parcselect_Label_Installation' ) && ! Parcselect_Label_Installation::owns( $order ) ) {
			if ( Parcselect_Label_Installation::has_any_label_meta( $order ) ) {
				Parcselect_Label_Installation::purge_order( $order );
			}
			return;
		}

		$tracking = $order->get_meta( '_ps_label_tracking_number', true );
		if ( empty( $tracking ) ) {
			return;
		}

		// Carrier-specific tracking URL (same logic as email).
		$carrier      = $order->get_meta( '_ps_label_carrier', true );
		$carrier      = $carrier ? $carrier : 'inpost';
		$tracking_url = class_exists( 'Parcselect_Label_Carrier' )
			? Parcselect_Label_Carrier::get_tracking_url( $carrier, $tracking )
			: 'https://inpost.pl/sledzenie-przesylek?number=' . $tracking;

		echo '<div style="margin:16px 0;padding:12px 16px;background:#f0f6fc;border-left:4px solid #2271b1;border-radius:4px;">';
		echo '<h3 style="margin:0 0 6px;font-size:14px;">' . esc_html__( 'Shipment tracking', 'parcel-selector' ) . '</h3>';
		echo '<p style="margin:0;font-size:13px;">';
		echo '<strong>' . esc_html__( 'Tracking number:', 'parcel-selector' ) . '</strong> ' . esc_html( $tracking );
		echo '<br><a href="' . esc_url( $tracking_url ) . '" target="_blank" style="color:#2271b1;">';
		echo esc_html__( 'Track your parcel', 'parcel-selector' ) . ' &rarr;</a>';
		echo '</p></div>';
	}

	// =========================================================================
	// 2. AUTO-STATUS → COMPLETED (called from do_generate_label)
	// =========================================================================

	/**
	 * Change order status to Completed after label is generated.
	 *
	 * @param int $order_id WooCommerce order ID.
	 */
	public static function auto_complete_order( $order_id ) {
		$order = wc_get_order( $order_id );
		if ( ! $order ) {
			return;
		}

		// Only auto-complete if order is processing.
		if ( 'processing' !== $order->get_status() ) {
			return;
		}

		$order->update_status( 'completed', __( 'Auto-completed: shipping label generated via the Label tool.', 'parcel-selector' ) );
	}

	// =========================================================================
	// 3. CRON — CLEANUP EXPIRED SESSIONS
	// =========================================================================

	/**
	 * Schedule cleanup crons.
	 *
	 * Cleanup runs twice daily (every 12h) — hourly was overkill: a stuck
	 * payment session expires after 24h, so checking 24× per day delivers
	 * no faster recovery than checking 2×. Retry runs twice daily too,
	 * 6h offset, so the two cron handlers never collide.
	 *
	 * Plus: re-clear any legacy hourly schedule from older versions so a
	 * plugin update naturally migrates existing sites to the lighter
	 * cadence without admin intervention.
	 */
	public static function schedule_cleanup() {
		// Migrate from legacy hourly cadence — clear and re-schedule fresh.
		$ts_cleanup = wp_next_scheduled( 'parcselect_label_cleanup_expired' );
		if ( $ts_cleanup && 'twicedaily' !== wp_get_schedule( 'parcselect_label_cleanup_expired' ) ) {
			wp_clear_scheduled_hook( 'parcselect_label_cleanup_expired' );
			$ts_cleanup = false;
		}
		if ( ! $ts_cleanup ) {
			wp_schedule_event( time() + 600, 'twicedaily', 'parcselect_label_cleanup_expired' );
		}

		$ts_retry = wp_next_scheduled( 'parcselect_label_retry_failed' );
		if ( $ts_retry && 'twicedaily' !== wp_get_schedule( 'parcselect_label_retry_failed' ) ) {
			wp_clear_scheduled_hook( 'parcselect_label_retry_failed' );
			$ts_retry = false;
		}
		if ( ! $ts_retry ) {
			wp_schedule_event( time() + 6 * HOUR_IN_SECONDS, 'twicedaily', 'parcselect_label_retry_failed' );
		}

		// Monthly PDF prune — keeps uploads/ps-labels from growing
		// unbounded. Tracking_number meta survives so the order page
		// keeps the audit trail; only the locally-mirrored PDF blob is
		// dropped after 90 days.
		if ( ! wp_next_scheduled( 'parcselect_label_prune_old_pdfs' ) ) {
			wp_schedule_event( time() + DAY_IN_SECONDS, 'daily', 'parcselect_label_prune_old_pdfs' );
		}
	}

	/**
	 * Expire sessions stuck in awaiting_payment > 24 hours.
	 *
	 * Cheap COUNT short-circuit before the expensive wc_get_orders fetch:
	 * a sklep with zero orders carrying _ps_label_status meta does no
	 * postmeta scan at all. Saves ~140 ms × 730 cron tików/year on
	 * stores that never use the label feature.
	 */
	public static function cleanup_expired_sessions() {
		global $wpdb;
		$pending_count = (int) $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- bezposrednie zapytanie do tabeli HPOS WooCommerce, brak API; wynik jednorazowy, cache zbedny
			$wpdb->prepare(
				"SELECT COUNT(*) FROM {$wpdb->postmeta} WHERE meta_key = %s AND meta_value = %s",
				'_ps_label_status',
				'awaiting_payment'
			)
		);
		if ( 0 === $pending_count ) {
			// HPOS check too — return only if both tables are empty.
			$hpos_table = $wpdb->prefix . 'wc_orders_meta';
			$hpos_exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $hpos_table ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- bezposrednie zapytanie do tabeli HPOS WooCommerce, brak API; wynik jednorazowy, cache zbedny
			if ( ! $hpos_exists ) {
				return;
			}
			$pending_hpos = (int) $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- nazwa tabeli HPOS z $wpdb->prefix (zaufana); wartosci przez $wpdb->prepare()
				$wpdb->prepare(
					"SELECT COUNT(*) FROM {$hpos_table} WHERE meta_key = %s AND meta_value = %s", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- nazwa tabeli HPOS z $wpdb->prefix (zaufana, nie input); wartosci przez %s.
					'_ps_label_status',
					'awaiting_payment'
				)
			);
			if ( 0 === $pending_hpos ) {
				return;
			}
		}

		$orders = wc_get_orders( array(
			'limit'      => 50,
			'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- zapytanie administracyjne, nie hot-path
				array(
					'key'   => '_ps_label_status',
					'value' => 'awaiting_payment',
				),
			),
		) );

		$cutoff = time() - ( 24 * HOUR_IN_SECONDS );

		foreach ( $orders as $order ) {
			// Skip orders whose label meta belongs to a different installation.
			if ( class_exists( 'Parcselect_Label_Installation' ) && ! Parcselect_Label_Installation::owns( $order ) ) {
				Parcselect_Label_Installation::purge_order( $order );
				continue;
			}
			$created = $order->get_meta( '_ps_label_created_at', true );
			if ( empty( $created ) ) {
				continue;
			}

			$created_ts = strtotime( $created );
			if ( $created_ts && $created_ts < $cutoff ) {
				// Clear all etykietownik meta — return to initial state.
				$keys = array(
					'_ps_label_session_id', '_ps_label_status', '_ps_label_size',
					'_ps_label_weight', '_ps_label_base_price', '_ps_label_price_pln',
					'_ps_label_price_display', '_ps_label_sol_amount', '_ps_label_sol_rate',
					'_ps_label_sol_wallet', '_ps_label_qr_uri', '_ps_label_created_at',
				);
				foreach ( $keys as $key ) {
					$order->delete_meta_data( $key );
				}
				$order->save();

				$order->add_order_note( __( 'Label tool: payment session expired after 24h. Reset to initial state.', 'parcel-selector' ) );
			}
		}
	}

	// =========================================================================
	// 3b'. CRON — PRUNE OLD LOCAL PDF MIRRORS
	// =========================================================================

	/**
	 * Delete locally-mirrored label PDFs in uploads/ps-labels older than
	 * 90 days. The label has long been delivered by then; the order's
	 * tracking_number meta survives so the audit trail stays intact, only
	 * the PDF blob is removed. Without this, a sklep generating 1k labels/year
	 * accumulates ~240 MB of dead PDFs on disk indefinitely.
	 */
	public static function prune_old_pdf_mirrors() {
		$_uploads = wp_upload_dir();
		if ( empty( $_uploads['basedir'] ) ) {
			return;
		}
		$dir = trailingslashit( $_uploads['basedir'] ) . 'ps-labels';
		if ( ! is_dir( $dir ) ) {
			return;
		}
		$cutoff = time() - 90 * DAY_IN_SECONDS;
		$pruned = 0;
		foreach ( (array) glob( $dir . '/*.pdf' ) as $pdf ) {
			$mtime = @filemtime( $pdf ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- file may be racing with another process.
			if ( $mtime && $mtime < $cutoff ) {
				wp_delete_file( $pdf );
				$pruned++;
			}
		}
		if ( $pruned > 0 && class_exists( 'Parcselect_Label_Debug' ) ) {
			Parcselect_Label_Debug::info( null, 'cron.prune_old_pdfs', array(
				'pruned'    => $pruned,
				'older_than_days' => 90,
				'dir'       => $dir,
			) );
		}
	}

	// =========================================================================
	// 3b. CRON — RETRY FAILED LABEL GENERATIONS
	// =========================================================================

	/**
	 * Retry labels stuck in 'error' or 'paid' (payment confirmed but generation failed).
	 * Runs twicedaily. Max 3 retries per order (tracked via _ps_label_retry_count).
	 *
	 * Cheap COUNT short-circuit before the wc_get_orders fetch — most stores
	 * never have a stuck label, so the cron returns instantly without scanning.
	 */
	public static function retry_failed_labels() {
		global $wpdb;
		$candidates = (int) $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- bezposrednie zapytanie do tabeli HPOS WooCommerce, brak API; wynik jednorazowy, cache zbedny
			$wpdb->prepare(
				"SELECT COUNT(*) FROM {$wpdb->postmeta} WHERE meta_key = %s AND meta_value IN ('error','paid','generating')",
				'_ps_label_status'
			)
		);
		if ( 0 === $candidates ) {
			$hpos_table = $wpdb->prefix . 'wc_orders_meta';
			$hpos_exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $hpos_table ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- bezposrednie zapytanie do tabeli HPOS WooCommerce, brak API; wynik jednorazowy, cache zbedny
			if ( ! $hpos_exists ) {
				return;
			}
			$candidates_hpos = (int) $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- nazwa tabeli HPOS z $wpdb->prefix (zaufana); wartosci przez $wpdb->prepare()
				$wpdb->prepare(
					"SELECT COUNT(*) FROM {$hpos_table} WHERE meta_key = %s AND meta_value IN ('error','paid','generating')", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- nazwa tabeli HPOS z $wpdb->prefix (zaufana, nie input); wartosci przez %s.
					'_ps_label_status'
				)
			);
			if ( 0 === $candidates_hpos ) {
				return;
			}
		}
		$orders = wc_get_orders( array(
			'limit'      => 10,
			'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- zapytanie administracyjne, nie hot-path
				array(
					'key'     => '_ps_label_status',
					'value'   => array( 'error', 'paid', 'generating' ),
					'compare' => 'IN',
				),
				array(
					'key'     => '_ps_label_txid',
					'compare' => 'EXISTS',
				),
			),
		) );

		foreach ( $orders as $order ) {
			// Skip retries for orders that belong to another installation.
			if ( class_exists( 'Parcselect_Label_Installation' ) && ! Parcselect_Label_Installation::owns( $order ) ) {
				Parcselect_Label_Installation::purge_order( $order );
				continue;
			}
			$status = $order->get_meta( '_ps_label_status', true );
			$txid   = $order->get_meta( '_ps_label_txid', true );

			// Only retry orders that were actually paid.
			if ( empty( $txid ) ) {
				continue;
			}

			// R8 (Konstytucja) — MONEY-BACK GUARANTEE. Zamowienie OPLACONE
			// (jest txid) ktore po >24h wciaz nie ma etykiety: oznacz
			// 'refund_pending', dodaj notatke, przerwij dalsze retry. Admin
			// dostaje czerwony alert (render_refund_pending_notice) — system
			// sam wykrywa "klient zaplacil, nie ma etykiety", admin nie musi
			// tego pilnowac.
			$_has_label = ( (int) $order->get_meta( '_ps_label_pdf_db_id', true ) > 0 )
				|| ( '' !== (string) $order->get_meta( '_ps_label_tracking_number', true ) );
			if ( ! $_has_label ) {
				$_paid_raw = (string) $order->get_meta( '_ps_label_paid_at', true );
				if ( '' === $_paid_raw ) {
					$_paid_raw = (string) $order->get_meta( '_ps_label_created_at', true );
				}
				$_paid_ts = $_paid_raw ? strtotime( $_paid_raw ) : 0;
				if ( $_paid_ts > 0 && ( time() - $_paid_ts ) > DAY_IN_SECONDS ) {
					$order->update_meta_data( '_ps_label_status', 'refund_pending' );
					$order->update_meta_data( '_ps_label_refund_flagged_at', current_time( 'mysql' ) );
					$order->save();
					delete_transient( 'parcselect_refund_pending_count' );
					self::log( 'ESCALATE order #' . $order->get_id() . ' -> refund_pending (paid, no label > 24h)' );
					$order->add_order_note(
						__( 'Label tool: payment confirmed but no shipping label was generated within 24 hours. Order flagged as "refund_pending" — see the red ParcelSelector admin notice. Open the order to retry from the Oracle or issue a refund.', 'parcel-selector' )
					);
					continue; // Czeka na decyzje admina — nie probujemy juz retry.
				}
			}

			// Skip 'generating' unless stuck > 10 minutes.
			if ( 'generating' === $status ) {
				$paid_at = $order->get_meta( '_ps_label_paid_at', true );
				if ( $paid_at && ( time() - strtotime( $paid_at ) ) < 600 ) {
					continue; // Still processing, give it time.
				}
			}

			// Max 3 retries.
			$retries = (int) $order->get_meta( '_ps_label_retry_count', true );
			if ( $retries >= 3 ) {
				continue;
			}

			$order->update_meta_data( '_ps_label_retry_count', $retries + 1 );
			$order->delete_meta_data( '_ps_label_error' );
			$order->save();

			self::log( 'RETRY #' . ( $retries + 1 ) . ' for order #' . $order->get_id() . ' (status was: ' . $status . ')' );

			$order->add_order_note(
				/* translators: %d: retry attempt number */
				sprintf( __( 'Label tool: auto-retry #%d — regenerating label.', 'parcel-selector' ), $retries + 1 )
			);

			Parcselect_Label_Ajax::do_generate_label( $order->get_id() );
		}
	}

	// =========================================================================
	// 4. R8 — MONEY-BACK GUARANTEE: red admin notice for stuck paid orders
	// =========================================================================

	/**
	 * R8 (Konstytucja flow platnosc->etykieta) — czerwony alert admina gdy
	 * istnieja zamowienia w statusie 'refund_pending' (oplacone, bez etykiety
	 * > 24h — patrz retry_failed_labels). Zasada: admin ma byc POWIADAMIANY,
	 * nie odpowiedzialny za wykrycie problemu.
	 *
	 * Licznik cache'owany w transiencie 5 min — admin_notices odpala sie na
	 * KAZDEJ stronie wp-admin, wiec nie liczymy COUNT za kazdym razem.
	 */
	public static function render_refund_pending_notice() {
		if ( ! current_user_can( 'edit_shop_orders' ) ) {
			return;
		}
		$count = get_transient( 'parcselect_refund_pending_count' );
		if ( false === $count ) {
			global $wpdb;
			$count = (int) $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- jednorazowy COUNT, wynik cache'owany w transiencie 5 min.
				$wpdb->prepare(
					"SELECT COUNT(*) FROM {$wpdb->postmeta} WHERE meta_key = %s AND meta_value = %s",
					'_ps_label_status',
					'refund_pending'
				)
			);
			$hpos_table = $wpdb->prefix . 'wc_orders_meta';
			$hpos_exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $hpos_table ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- sprawdzenie istnienia tabeli HPOS.
			if ( $hpos_exists ) {
				$count += (int) $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- nazwa tabeli HPOS z $wpdb->prefix (zaufana); wartosci przez prepare().
					$wpdb->prepare(
						"SELECT COUNT(*) FROM {$hpos_table} WHERE meta_key = %s AND meta_value = %s", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- nazwa tabeli HPOS z $wpdb->prefix (zaufana, nie input); wartosci przez %s.
						'_ps_label_status',
						'refund_pending'
					)
				);
			}
			set_transient( 'parcselect_refund_pending_count', (int) $count, 5 * MINUTE_IN_SECONDS );
		}
		$count = (int) $count;
		if ( $count < 1 ) {
			return;
		}
		printf(
			'<div class="notice notice-error" style="border-left-width:8px;"><p style="font-size:14px;font-weight:700;margin:0 0 4px">%s</p><p style="margin:0 0 8px">%s</p><p style="margin:0"><a href="%s" class="button button-primary">%s</a></p></div>',
			esc_html__( 'ParcelSelector — paid orders without a shipping label', 'parcel-selector' ),
			esc_html(
				sprintf(
					/* translators: %d: number of orders paid but without a label */
					_n(
						'%d order has a confirmed payment but received no shipping label within 24 hours. The customer is waiting — open the order and use "Retrieve the label from the Oracle", or issue a refund.',
						'%d orders have a confirmed payment but received no shipping label within 24 hours. The customers are waiting — open each order and use "Retrieve the label from the Oracle", or issue a refund.',
						$count,
						'parcel-selector'
					),
					$count
				)
			),
			esc_url( admin_url( 'admin.php?page=wc-orders' ) ),
			esc_html__( 'Open WooCommerce orders', 'parcel-selector' )
		);
	}

	// =========================================================================
	// 5. VALIDATION — check data before sending to shipping provider
	// =========================================================================

	/**
	 * Validate order data before creating shipping provider package.
	 *
	 * @param int $order_id Order ID.
	 * @return array [ 'ok' => bool, 'errors' => string[] ]
	 */
	public static function validate_order_for_label( $order_id ) {
		$errors = array();
		$order  = wc_get_order( $order_id );

		if ( ! $order ) {
			return array( 'ok' => false, 'errors' => array( 'Order not found' ) );
		}

		// Receiver data.
		$first = $order->get_shipping_first_name();
		$last  = $order->get_shipping_last_name();
		if ( empty( trim( $first . $last ) ) ) {
			$first = $order->get_billing_first_name();
			$last  = $order->get_billing_last_name();
		}
		if ( empty( trim( $first . $last ) ) ) {
			$errors[] = __( 'Missing receiver name (shipping or billing).', 'parcel-selector' );
		}

		$phone = $order->get_billing_phone();
		if ( empty( $phone ) ) {
			$errors[] = __( 'Missing phone number.', 'parcel-selector' );
		}

		$email = $order->get_billing_email();
		if ( empty( $email ) ) {
			$errors[] = __( 'Missing email address.', 'parcel-selector' );
		}

		$locker = $order->get_meta( '_ps_locker_name', true );
		if ( empty( $locker ) ) {
			$errors[] = __( 'No pickup point (paczkomat) selected.', 'parcel-selector' );
		}

		// Sender — shipping provider ALWAYS requires full sender address.
		// Use etykietownik settings with WC store fallbacks.
		$sender_name   = get_option( 'parcselect_label_sender_name', '' ) ?: get_option( 'blogname', '' );
		$sender_street = get_option( 'parcselect_label_sender_street', '' ) ?: get_option( 'woocommerce_store_address', '' );
		$sender_city   = get_option( 'parcselect_label_sender_city', '' ) ?: get_option( 'woocommerce_store_city', '' );

		if ( empty( $sender_name ) ) {
			$errors[] = __( 'Sender name not configured. Go to Parcel Selector → Label tool → Sender settings.', 'parcel-selector' );
		}
		if ( empty( $sender_street ) ) {
			$errors[] = __( 'Sender street not configured. Set in Label tool settings or WooCommerce Store Address.', 'parcel-selector' );
		}
		if ( empty( $sender_city ) ) {
			$errors[] = __( 'Sender city not configured. Set in Label tool settings or WooCommerce Store Address.', 'parcel-selector' );
		}

		// Drop-off point for parcel locker carriers.
		$carrier = 'inpost';
		if ( class_exists( 'Parcselect_Label_Carrier' ) ) {
			$carrier = Parcselect_Label_Carrier::detect( $order );
		}
		if ( in_array( $carrier, array( 'inpost' ), true ) ) {
			$_pickup = get_option( 'parcselect_label_pickup_' . $carrier, '' );
			if ( empty( $_pickup ) ) {
				/* translators: %s: carrier name */
				$errors[] = sprintf( __( '%s drop-off locker not configured. Go to Parcel Selector → Label tool → Drop-off points.', 'parcel-selector' ), ucfirst( $carrier ) );
			}
		}

		// Label service — in VPS proxy mode, service_id + credentials are on VPS server.
		$use_direct = '1' === get_option( 'parcselect_label_direct_api', '0' );
		if ( $use_direct ) {
			$service_id = get_option( 'parcselect_label_service_id', '' );
			if ( empty( $service_id ) ) {
				$errors[] = __( 'Label Service ID not configured.', 'parcel-selector' );
			}
			$client_id = get_option( 'parcselect_label_client_id', '' );
			if ( empty( $client_id ) ) {
				$errors[] = __( 'Label API credentials not configured.', 'parcel-selector' );
			}
		}

		return array(
			'ok'     => empty( $errors ),
			'errors' => $errors,
		);
	}

	// =========================================================================
	// 6. GUARD — only show metabox for PS locker orders
	// =========================================================================

	/**
	 * Check if order used ParcelSelector shipping method.
	 *
	 * @param WC_Order $order Order object.
	 * @return bool
	 */
	public static function is_ps_order( $order ) {
		$items = $order->get_items( 'shipping' );
		foreach ( $items as $item ) {
			if ( false !== strpos( $item->get_method_id(), 'parcselect_inpost_paczkomat' ) ) {
				return true;
			}
		}

		// Fallback: check if locker meta exists.
		$locker = $order->get_meta( '_ps_locker_name', true );
		return ! empty( $locker );
	}

	// =========================================================================
	// 8. LOGGING
	// =========================================================================

	/**
	 * Log API interaction to WooCommerce logger.
	 *
	 * @param string $message Log message.
	 * @param string $level   Log level (info, warning, error).
	 */
	public static function log( $message, $level = 'info' ) {
		if ( ! function_exists( 'wc_get_logger' ) ) {
			return;
		}
		$logger = wc_get_logger();
		$logger->log( $level, $message, array( 'source' => 'etykietownik' ) );
	}

	// =========================================================================
	// 10. ORDERS LIST COLUMN
	// =========================================================================

	/**
	 * Add "Label" column to orders list.
	 *
	 * @param array $columns Existing columns.
	 * @return array Modified columns.
	 */
	public static function add_orders_column( $columns ) {
		$new = array();
		foreach ( $columns as $key => $label ) {
			$new[ $key ] = $label;
			// Insert after 'order_status' column.
			if ( 'order_status' === $key ) {
				$new['parcselect_label_label'] = __( 'Label', 'parcel-selector' );
			}
		}
		return $new;
	}

	/**
	 * Render "Label" column content (classic CPT orders).
	 *
	 * @param string $column  Column key.
	 * @param int    $post_id Post ID.
	 */
	public static function render_orders_column( $column, $post_id ) {
		if ( 'parcselect_label_label' !== $column ) {
			return;
		}
		$order = wc_get_order( $post_id );
		if ( $order ) {
			self::output_column_content( $order );
		}
	}

	/**
	 * Render "Label" column content (HPOS orders).
	 *
	 * @param string   $column Column key.
	 * @param WC_Order $order  Order object.
	 */
	public static function render_orders_column_hpos( $column, $order ) {
		if ( 'parcselect_label_label' !== $column ) {
			return;
		}
		self::output_column_content( $order );
	}

	/**
	 * Output column content — icon + tracking.
	 *
	 * @param WC_Order $order Order object.
	 */
	private static function output_column_content( $order ) {
		// Cross-install leak guard — never paint a "Label ✓" badge in the WC
		// orders list using meta written by a different installation.
		if ( class_exists( 'Parcselect_Label_Installation' )
			&& ! Parcselect_Label_Installation::owns( $order )
			&& Parcselect_Label_Installation::has_any_label_meta( $order ) ) {
			Parcselect_Label_Installation::purge_order( $order );
		}
		$status   = $order->get_meta( '_ps_label_status', true );
		$tracking = $order->get_meta( '_ps_label_tracking_number', true );
		$locker   = $order->get_meta( '_ps_locker_name', true );
		$carrier  = $order->get_meta( '_ps_label_carrier', true );
		if ( ! $carrier ) {
			$carrier = class_exists( 'Parcselect_Label_Carrier' ) ? Parcselect_Label_Carrier::detect( $order ) : 'inpost';
		}

		// Carrier label.
		$carrier_labels = array(
			'inpost' => 'InPost',
		);
		$carrier_short = isset( $carrier_labels[ $carrier ] ) ? $carrier_labels[ $carrier ] : '';

		if ( empty( $locker ) ) {
			echo '<span style="color:#ddd">&mdash;</span>';
			return;
		}

		if ( 'completed' === $status ) {
			// Carrier badge.
			echo '<span style="display:inline-block;background:#dcfce7;color:#166534;font-size:10px;font-weight:700;padding:2px 6px;border-radius:4px;margin-bottom:2px;">&#x2705; ' . esc_html( $carrier_short ) . '</span>';

			// Tracking number — clickable link to carrier tracking page.
			if ( ! empty( $tracking ) ) {
				$tracking_url = class_exists( 'Parcselect_Label_Carrier' )
					? Parcselect_Label_Carrier::get_tracking_url( $carrier, $tracking )
					: 'https://inpost.pl/sledzenie-przesylek?number=' . $tracking;
				echo '<br><a href="' . esc_url( $tracking_url ) . '" target="_blank" title="' . esc_attr__( 'Track shipment', 'parcel-selector' ) . '" style="font-size:10px;color:#2563eb;font-family:monospace;text-decoration:underline;">' . esc_html( $tracking ) . '</a>';
			}
		} elseif ( in_array( $status, array( 'awaiting_payment', 'paid', 'generating' ), true ) ) {
			echo '<span title="' . esc_attr( $status ) . '">&#x23F3;</span>';
			if ( $carrier_short ) {
				echo ' <small style="font-size:9px;color:#a16207;font-weight:700">' . esc_html( $carrier_short ) . '</small>';
			}
		} elseif ( 'error' === $status ) {
			echo '<span title="Error">&#x274C;</span>';
			if ( $carrier_short ) {
				echo ' <small style="font-size:9px;color:#dc2626;font-weight:700">' . esc_html( $carrier_short ) . '</small>';
			}
		} else {
			echo '<span title="' . esc_attr__( 'Ready for label', 'parcel-selector' ) . '">&#x1F4E6;</span>';
			if ( $carrier_short ) {
				echo ' <small style="font-size:9px;color:#6b7280">' . esc_html( $carrier_short ) . '</small>';
			}
		}
	}
}

Parcselect_Label_Hooks::init();
