<?php
/**
 * ETYKIETOWNIK — Label generation module for ParcelSelector.
 *
 * Creates InPost shipping labels via the ParcelSelector label API.
 * Payment in Solana (SOL) via the VPS hosted gateway at api.parcelselector.com.
 * No dependency on any other plugin — the VPS is the single source of truth
 * for payment status and label generation.
 *
 * @package ParcelSelector
 * @since   3.4.0
 */

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

// phpcs:disable WordPress.WP.I18n.MissingTranslatorsComment -- Translators comments are placed before sprintf() wrapper, not before each individual __()/_e() call

if ( ! defined( 'PARCSELECT_LABEL_DIR' ) ) {
	define( 'PARCSELECT_LABEL_DIR', plugin_dir_path( __FILE__ ) );
}
if ( ! defined( 'PARCSELECT_LABEL_URL' ) ) {
	define( 'PARCSELECT_LABEL_URL', plugin_dir_url( __FILE__ ) );
}

// Load module classes.
//
// Required modules — fatal if missing because the iframe + AJAX flow can't
// run without them. WP fatal recovery surfaces a clear path-error in the
// admin notice rather than silently dying.
require_once PARCSELECT_LABEL_DIR . 'class-ps-label-debug.php';  // Central multi-sink logger (first — others depend on it).
require_once PARCSELECT_LABEL_DIR . 'class-ps-label-installation.php';  // Installation-scoped ownership guard.
require_once PARCSELECT_LABEL_DIR . 'class-ps-label-session-client.php';  // map-server /embed/label/* client.
require_once PARCSELECT_LABEL_DIR . 'class-ps-label-enrollment.php';  // Self-register with VPS, pull credentials.
require_once PARCSELECT_LABEL_DIR . 'class-ps-label-autotrigger.php';  // woocommerce_payment_complete listener.
Parcselect_Label_Debug::init();
// v4.2.1 BEZWZGLEDNY consent gate — Enrollment::init() commented out.
// Enrollment wywolywany TYLKO po Stage 1 consent click (przez Parcselect_Label_Embed::ajax_create_session).
// Parcselect_Label_Enrollment::init();  // DISABLED dla compliance

// Show admin notice if enrollment to label service failed.
add_action( 'admin_notices', function() {
	$err = get_transient( 'parcselect_label_enroll_error' );
	if ( $err && current_user_can( 'manage_options' ) ) {
		echo '<div class="notice notice-warning is-dismissible"><p>';
		echo '<strong>' . esc_html__( 'Parcel Selector — Label tool:', 'parcel-selector' ) . '</strong> ';
		echo esc_html__( 'Label service connection failed: ', 'parcel-selector' ) . esc_html( $err );
		echo ' <em>' . esc_html__( 'Will retry automatically.', 'parcel-selector' ) . '</em>';
		echo '</p></div>';
	}
} );

// v4.2.4 — Oracle URL fail-fast notice na ekranach order edit.
// Cel: admin widzi ostrzezenie ZANIM klient kliknie Stage 4 i dostanie DOCTYPE timeout.
// Cache transient 60s zeby nie spamowac HTTP HEAD przy kazdym page load.
add_action( 'admin_notices', function() {
	$screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
	if ( ! $screen ) {
		return;
	}
	$relevant = array(
		'woocommerce_page_wc-orders',
		'shop_order',
		'edit-shop_order',
	);
	if ( ! in_array( $screen->id, $relevant, true ) ) {
		return;
	}
	if ( ! current_user_can( 'edit_shop_orders' ) ) {
		return;
	}

	$url = (string) get_option( 'parcselect_label_oracle_url', '' );
	if ( '' === $url ) {
		echo '<div class="notice notice-error"><p><strong>' . esc_html__( 'ParcelSelector label tool:', 'parcel-selector' ) . '</strong> ';
		echo esc_html__( 'The label service URL is not configured — label generation will be unavailable.', 'parcel-selector' );
		echo ' ' . esc_html__( 'Set the label service URL in the Label tool settings.', 'parcel-selector' );
		echo '</p></div>';
		return;
	}

	$cache_key = 'parcselect_oracle_ping_' . md5( $url );
	$cached    = get_transient( $cache_key );
	if ( false === $cached ) {
		$resp = wp_remote_get( rtrim( $url, '/' ) . '/api/v1/health', array(
			'timeout'   => 3,
			'sslverify' => apply_filters( 'parcselect_label_sslverify', true ),
		) );
		$code   = is_wp_error( $resp ) ? 0 : (int) wp_remote_retrieve_response_code( $resp );
		$cached = ( 200 === $code ) ? 'OK' : 'FAIL_' . $code;
		set_transient( $cache_key, $cached, 60 );
	}

	if ( 'OK' !== $cached ) {
		echo '<div class="notice notice-warning"><p><strong>' . esc_html__( 'ParcelSelector label tool:', 'parcel-selector' ) . '</strong> ';
		echo esc_html( sprintf( /* translators: 1: label service URL, 2: failure status */ __( 'The label service at %1$s is not responding (%2$s).', 'parcel-selector' ), $url, $cached ) );
		echo ' ' . esc_html__( 'Label generation may be temporarily unavailable. Verify the label service URL in the Label tool settings.', 'parcel-selector' );
		echo '</p></div>';
	}
} );

require_once PARCSELECT_LABEL_DIR . 'class-ps-label-pricing.php';
require_once PARCSELECT_LABEL_DIR . 'class-pscp-oracle-client.php'; // v4.1 B1
// v4.2 — A1: SPEC/11 CONSENT_AND_TERMS — kanoniczna polityka zgody w Stage 1 (PIERWSZE okienko).
// Loaded BEFORE class-ps-label-metabox.php so render_stages can reference Terms.
require_once PARCSELECT_LABEL_DIR . 'class-ps-label-terms.php';
require_once PARCSELECT_LABEL_DIR . 'class-ps-label-consent-log.php';
require_once PARCSELECT_LABEL_DIR . 'class-ps-label-consent-gate.php';
require_once PARCSELECT_LABEL_DIR . 'class-ps-label-pdf-store.php';     // v4.4.5 M3: PDF w tabeli DB (nie uploads/).
require_once PARCSELECT_LABEL_DIR . 'class-ps-label-pdf-endpoint.php';  // v4.4.5 M3: uwierzytelniony endpoint pobierania PDF.
require_once PARCSELECT_LABEL_DIR . 'class-ps-label-identity.php';   // v4.2 E.1: SPEC/05 FEATURE-17 single source of truth installation_id.
Parcselect_Label_Terms::init();
Parcselect_Label_Consent_Log::init();
Parcselect_Label_Consent_Gate::init();
Parcselect_Label_Pdf_Store::init();
Parcselect_Label_Pdf_Endpoint::init();
Parcselect_Label_Identity::init();
// v4.1.2 (2026-04-25): PS_Label_Metabox REAKTYWOWANE — posiada zielone UI + embed PDF + print.
// Delegates Krok 2 QR/payment do Parcselect_Label_Embed::render_from_pscp_payload (Oracle payload).
require_once PARCSELECT_LABEL_DIR . 'class-ps-label-metabox.php';
require_once PARCSELECT_LABEL_DIR . 'class-ps-label-stepper.php';
// NOTE: legacy comment kept for archaeology:
// class-ps-label-metabox.php usuniety: zastepuje includes/class-ps-label-embed.php — UNDONE v4.1.2
// (port 1:1 z PRO commit 18cb575 — iframe + postMessage bridge, wszystko z VPS).
require_once PARCSELECT_LABEL_DIR . 'class-ps-label-ajax.php';

// Optional modules — present in the dev tree but tolerable if a deploy strips
// them (legacy native-flow files from the pre-iframe pivot). Each include is
// guarded so a missing file logs a warning to the trail instead of fatal'ing
// out — the iframe flow and AJAX endpoints work without these.
$parcselect_label_optional = array(
	'class-ps-label-api.php'      => 'Parcselect_Label_API',
	'class-ps-label-crypto.php'   => 'Parcselect_Label_Crypto',
	'class-ps-label-settings.php' => 'Parcselect_Label_Settings',
	'class-ps-label-email.php'    => 'Parcselect_Label_Email',
	'class-ps-label-hooks.php'    => 'Parcselect_Label_Hooks',
	'class-ps-label-carrier.php'  => 'Parcselect_Label_Carrier',
);
foreach ( $parcselect_label_optional as $parcselect_file => $parcselect_class ) {
	$path = PARCSELECT_LABEL_DIR . $parcselect_file;
	if ( file_exists( $path ) ) {
		require_once $path;
	} else {
		Parcselect_Label_Debug::warn( null, 'init.optional_module_missing', array(
			'file'  => $parcselect_file,
			'class' => $parcselect_class,
			'note'  => 'iframe flow + core AJAX still work without it; legacy/native fallback features that need this class will no-op',
		) );
	}
}
unset( $parcselect_label_optional );

// Stubs for legacy classes — guarantee that an order screen never fatals
// with "Class PS_Label_API not found" if a deploy stripped the optional
// module files. Each stub no-ops with a debug-trail breadcrumb so the
// admin sees `init.legacy_stub_called` in the accordion instead of WSOD.
if ( ! class_exists( 'Parcselect_Label_API' ) ) {
	class Parcselect_Label_API {
		const TOKEN_CACHE = 'parcselect_label_api_token_unused';
		const SIZES       = array(
			'A' => array( 'label' => 'A (8x38x64)',  'max_weight' => 25, 'description' => 'Small' ),
			'B' => array( 'label' => 'B (19x38x64)', 'max_weight' => 25, 'description' => 'Medium' ),
			'C' => array( 'label' => 'C (41x38x64)', 'max_weight' => 25, 'description' => 'Large' ),
		);
	}
	Parcselect_Label_Debug::warn( null, 'init.legacy_stub_loaded', array( 'class' => 'Parcselect_Label_API' ) );
}
// PS_Label_API stub loaded above — no alias needed.
if ( ! class_exists( 'Parcselect_Label_Hooks' ) ) {
	class Parcselect_Label_Hooks {
		public static function log( $msg, $level = 'info' ) {
			if ( class_exists( 'Parcselect_Label_Debug' ) ) {
				Parcselect_Label_Debug::log( null, 'legacy.hooks_log', array( 'msg' => substr( (string) $msg, 0, 300 ) ), $level );
			}
		}
		public static function validate_order_for_label( $order_id ) {
			// No native validation available — trust caller, return success.
			return array( 'valid' => true, 'errors' => array() );
		}
	}
	Parcselect_Label_Debug::warn( null, 'init.legacy_stub_loaded', array( 'class' => 'Parcselect_Label_Hooks' ) );
}
if ( ! class_exists( 'Parcselect_Label_Crypto' ) ) {
	class Parcselect_Label_Crypto {
		public static function get_enabled_coins() { return array(); }
		public static function get_enabled_wallets() { return array(); }
		public static function convert_detailed() { return false; }
	}
	Parcselect_Label_Debug::warn( null, 'init.legacy_stub_loaded', array( 'class' => 'Parcselect_Label_Crypto' ) );
}
if ( ! class_exists( 'Parcselect_Label_Settings' ) ) {
	class Parcselect_Label_Settings { public static function init() {} }
}
if ( ! class_exists( 'Parcselect_Label_Email' ) ) {
	class Parcselect_Label_Email { public static function init() {} }
}
if ( ! class_exists( 'Parcselect_Label_Carrier' ) ) {
	class Parcselect_Label_Carrier { public static function init() {} }
}

// Seed non-sensitive defaults only. Credentials (installation_id, proxy_secret,
// webhook_secret, wallet) come from PS_Label_Enrollment via the VPS —
// never hardcoded here because a default-shared-secret is a security hole,
// every customer install needs its own per-merchant values.
add_action( 'init', 'parcselect_label_seed_defaults', 5 );
function parcselect_label_seed_defaults() {
	$defaults = array(
		'parcselect_label_proxy_url'     => 'https://map.parcelselector.com',
		'parcselect_label_direct_api'    => '0',
		'parcselect_label_enabled_coins' => array( 'sol' ),
	);
	foreach ( $defaults as $key => $val ) {
		if ( false === get_option( $key, false ) ) {
			add_option( $key, $val, '', false );
		}
	}
}

// Ensure labels directory exists and is protected.
add_action( 'admin_init', 'parcselect_label_secure_labels_dir' );

/**
 * Returns the absolute path (with trailing slash) of the labels storage
 * directory inside wp-content/uploads/parcselect/labels/.
 *
 * Per WP.org plugin guidelines we never write to the plugin folder itself —
 * uploads/ is the only WordPress-blessed writable location for runtime data.
 *
 * @return string
 */
if ( ! function_exists( 'parcselect_label_storage_dir' ) ) {
	function parcselect_label_storage_dir() {
		$uploads = wp_upload_dir();
		return trailingslashit( $uploads['basedir'] ) . 'parcselect/labels/';
	}
}

/**
 * Returns the public URL (with trailing slash) of the labels storage directory.
 * Used by code that needs to expose a download link (always behind a nonce-gated
 * proxy — direct hits hit the .htaccess deny rule).
 *
 * @return string
 */
if ( ! function_exists( 'parcselect_label_storage_url' ) ) {
	function parcselect_label_storage_url() {
		$uploads = wp_upload_dir();
		return trailingslashit( $uploads['baseurl'] ) . 'parcselect/labels/';
	}
}

/**
 * Create labels directory with security files if missing — under uploads/, not the
 * plugin folder. .htaccess + empty index.php prevent directory listing and direct
 * access; PDF downloads are mediated by the AJAX/REST proxy with nonce + capability checks.
 */
function parcselect_label_secure_labels_dir() {
	$dir = parcselect_label_storage_dir();
	if ( ! is_dir( $dir ) ) {
		wp_mkdir_p( $dir );
	}
	$htaccess         = $dir . '.htaccess';
	$htaccess_content = "Options -Indexes\nDeny from all\n";
	if ( ! file_exists( $htaccess ) || false === strpos( (string) @file_get_contents( $htaccess ), 'Deny from all' ) ) { // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_get_contents,WordPress.PHP.NoSilencedErrors.Discouraged -- checking security file may not be readable on first run.
		file_put_contents( $htaccess, $htaccess_content ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents -- creating security file.
	}
	$index = $dir . 'index.php';
	if ( ! file_exists( $index ) ) {
		file_put_contents( $index, "<?php\n// Silence is golden.\n" ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents -- creating security file.
	}
}

// Payment confirmation arrives via the VPS outbound webhook to
// /wp-json/ps-pay/v1/confirm (see includes/class-ps-label-webhook.php).
// This module is deliberately free of cross-plugin hooks — no PSCP, no PRO.

// CROSS-INSTALL LEAK GUARD — bulk sweep on every admin load. Cheap (one
// short-circuit DB read of an option) when there is nothing to do, brutal
// when the parcselect_installation_id changes vs. the last sweep marker.
// Necessary in addition to the activation hook because users who copy a
// site (DB + files together) skip activation entirely; their first admin
// hit must still wipe leaked _ps_label_* meta before the metabox renders.
add_action( 'admin_init', 'parcselect_label_sweep_on_admin_init', 1 );
function parcselect_label_sweep_on_admin_init() {
	if ( class_exists( 'Parcselect_Label_Installation' ) ) {
		Parcselect_Label_Installation::bulk_sweep_if_installation_changed();
		// v4.0.4 forced upgrade purge — wipes ALL _ps_label_* meta on first
		// admin hit after upgrading from < 4.0.4 (where polling could have
		// stamped foreign data as ours). Idempotent.
		Parcselect_Label_Installation::forced_legacy_purge_if_needed();
	}
}
