<?php
/**
 * Parcselect_Label_Pdf_Endpoint — uwierzytelniony endpoint pobierania PDF etykiety.
 *
 * v4.4.5 M3 — część refactoru składowania PDF (WP.org security review).
 *
 * Bajty PDF żyją w tabeli DB `{prefix}parcselect_label_pdf` (patrz
 * Parcselect_Label_Pdf_Store). Ten endpoint to JEDYNA droga ich pobrania.
 *
 * BEZPIECZEŃSTWO (wymóg recenzenta WP.org):
 *   - capability: current_user_can( 'edit_shop_orders' ) — parytet ze starymi
 *     endpointami download_label/download_pdf (decyzja koordynatora M3).
 *   - nonce: wp_verify_nonce( ..., 'parcselect_label_pdf_' . $order_id ) — per order.
 *   - cross-install leak guard (Parcselect_Label_Installation::owns).
 *   - brak bezpośredniego publicznego URL do pliku — wszystko przez admin-ajax.
 *
 * ŹRÓDŁA PDF (rozwiązywane po kolei):
 *   1. Tabela DB — rekord dla order_id (nowy model, preferowany).
 *   2. Stary plik `_ps_label_local_pdf_path` (kompatybilność wsteczna) —
 *      odczyt + LENIWA MIGRACJA bajtów do DB przy pierwszym pobraniu.
 *
 * Action AJAX: `parcselect_label_pdf` (tylko wp_ajax_, brak nopriv).
 *
 * @package ParcelSelectorFree
 */

defined( 'ABSPATH' ) || exit;

final class Parcselect_Label_Pdf_Endpoint {

	/**
	 * Capability wymagana do pobrania etykiety.
	 * v4.4.5 M3 cross-check: zmienione z 'manage_woocommerce' na
	 * 'edit_shop_orders' — parytet ze starymi endpointami download_label/
	 * download_pdf (class-ps-label-ajax.php, class-ps-label-embed.php).
	 * 'edit_shop_orders' obejmuje shop_manager; nadpisywalne filtrem.
	 */
	const CAP = 'edit_shop_orders';

	public static function init() {
		// Wyłącznie wp_ajax_ (zalogowany użytkownik) — BRAK wp_ajax_nopriv_.
		add_action( 'wp_ajax_parcselect_label_pdf', array( __CLASS__, 'handle' ) );
	}

	/**
	 * Obsługa żądania pobrania PDF. Strumieniuje PDF z DB (lub starego pliku)
	 * z nagłówkami application/pdf + Content-Disposition. Kończy przez exit.
	 */
	public static function handle() {
		$order_id = isset( $_GET['order_id'] ) ? absint( wp_unslash( $_GET['order_id'] ) ) : 0;
		$nonce    = isset( $_GET['nonce'] ) ? sanitize_text_field( wp_unslash( $_GET['nonce'] ) ) : '';
		$inline   = isset( $_GET['inline'] ) ? sanitize_text_field( wp_unslash( $_GET['inline'] ) ) : '';

		// 1. Nonce — per order. Nonce WP wygasa po 12-24h. Jesli wygasl/brak,
		// ale zadanie pochodzi od ZALOGOWANEGO pracownika sklepu z uprawnieniem —
		// NIE pokazuj martwego "Invalid security token". Przekieruj na URL ze
		// SWIEZYM nonce (etykieta ma byc dostepna ZAWSZE). Niezalogowany lub
		// nieuprawniony (proba CSRF) dostaje twarde 403. Po redirekcie zadanie
		// wraca z waznym nonce i przechodzi pelna sciezke kontroli ponizej.
		if ( ! wp_verify_nonce( $nonce, 'parcselect_label_pdf_' . $order_id ) ) {
			$cap = apply_filters( 'parcselect_label_pdf_capability', self::CAP );
			if ( $order_id > 0 && is_user_logged_in() && current_user_can( $cap )
				&& class_exists( 'Parcselect_Label_Pdf_Store' ) ) {
				$inline_self = ( '1' === $inline );
				wp_safe_redirect( Parcselect_Label_Pdf_Store::nonced_url( $order_id, $inline_self ), 302 );
				exit;
			}
			status_header( 403 );
			wp_die( esc_html__( 'Invalid security token.', 'parcel-selector' ), '', array( 'response' => 403 ) );
		}

		// 2. Capability — tylko personel sklepu.
		$cap = apply_filters( 'parcselect_label_pdf_capability', self::CAP );
		if ( ! current_user_can( $cap ) ) {
			status_header( 403 );
			wp_die( esc_html__( 'Unauthorized.', 'parcel-selector' ), '', array( 'response' => 403 ) );
		}

		$order = $order_id ? wc_get_order( $order_id ) : null;
		if ( ! $order ) {
			status_header( 404 );
			wp_die( esc_html__( 'Order not found.', 'parcel-selector' ), '', array( 'response' => 404 ) );
		}

		// 3. Cross-install leak guard — nie serwuj PDF z cudzej instalacji.
		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 );
			}
			if ( class_exists( 'Parcselect_Label_Debug' ) ) {
				Parcselect_Label_Debug::error( $order_id, 'pdf_endpoint.cross_install_blocked', array(
					'reason' => 'label meta does not belong to this installation',
				) );
			}
			status_header( 403 );
			wp_die( esc_html__( 'This label does not belong to this plugin installation.', 'parcel-selector' ), '', array( 'response' => 403 ) );
		}

		// Tryb wyświetlania: inline (podgląd w iframe) vs attachment (pobranie).
		$disposition = ( '1' === $inline ) ? 'inline' : 'attachment';

		// ── Źródło 1: tabela DB ──────────────────────────────────────────────
		$row = class_exists( 'Parcselect_Label_Pdf_Store' )
			? Parcselect_Label_Pdf_Store::get_db_pdf( $order_id )
			: null;
		if ( is_array( $row ) && isset( $row['pdf_blob'] ) ) {
			$pdf = (string) $row['pdf_blob'];
			if ( strlen( $pdf ) >= 1000 && 0 === strpos( $pdf, '%PDF' ) ) {
				if ( class_exists( 'Parcselect_Label_Debug' ) ) {
					Parcselect_Label_Debug::info( $order_id, 'pdf_endpoint.served_db', array(
						'db_id' => (int) $row['id'],
						'bytes' => strlen( $pdf ),
					) );
				}
				self::stream_pdf( $pdf, self::filename_for( $order ), $disposition, 'db' );
			}
			if ( class_exists( 'Parcselect_Label_Debug' ) ) {
				Parcselect_Label_Debug::warn( $order_id, 'pdf_endpoint.db_row_invalid', array(
					'bytes' => strlen( $pdf ),
				) );
			}
		}

		// ── Źródło 2: stary plik lokalny (kompatybilność wsteczna) ───────────
		$legacy_path = (string) $order->get_meta( '_ps_label_local_pdf_path', true );
		if ( '' !== $legacy_path && is_readable( $legacy_path ) ) {
			// PATH TRAVERSAL GUARD — plik musi leżeć w uploads/ lub w katalogu wtyczki.
			$real          = realpath( $legacy_path );
			$uploads       = wp_upload_dir();
			$allowed_bases = array(
				realpath( trailingslashit( $uploads['basedir'] ) ),
				defined( 'PARCSELECT_LABEL_DIR' ) ? realpath( PARCSELECT_LABEL_DIR ) : false,
			);
			$path_safe = false;
			if ( false !== $real ) {
				foreach ( $allowed_bases as $base ) {
					if ( ! $base ) {
						continue;
					}
					// v4.4.5 M3 cross-check: porównanie z separatorem na końcu
					// bazy, by katalog-rodzeństwo o nazwie z prefiksem (np.
					// `uploads-evil` vs `uploads`) nie dał false-positive.
					$base_sep = rtrim( $base, '/\\' ) . DIRECTORY_SEPARATOR;
					if ( 0 === strpos( $real . DIRECTORY_SEPARATOR, $base_sep ) ) {
						$path_safe = true;
						break;
					}
				}
			}
			if ( $path_safe ) {
				$pdf = file_get_contents( $real ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_get_contents -- reading legacy PDF binary; WP_Filesystem unreliable for binary blobs.
				if ( false !== $pdf && strlen( $pdf ) >= 1000 && 0 === strpos( $pdf, '%PDF' ) ) {
					// LENIWA MIGRACJA — przenieś bajty do DB, by następne pobranie
					// szło z DB i by stary plik dało się bezpiecznie usunąć.
					if ( class_exists( 'Parcselect_Label_Pdf_Store' ) ) {
						$migrated = Parcselect_Label_Pdf_Store::migrate_file_to_db( $order );
						if ( class_exists( 'Parcselect_Label_Debug' ) ) {
							Parcselect_Label_Debug::info( $order_id, 'pdf_endpoint.lazy_migrate', array(
								'result' => is_wp_error( $migrated ) ? $migrated->get_error_code() : 'ok',
							) );
						}
					}
					if ( class_exists( 'Parcselect_Label_Debug' ) ) {
						Parcselect_Label_Debug::info( $order_id, 'pdf_endpoint.served_legacy_file', array(
							'path'  => $real,
							'bytes' => strlen( $pdf ),
						) );
					}
					self::stream_pdf( $pdf, self::filename_for( $order ), $disposition, 'legacy_file' );
				}
			} elseif ( class_exists( 'Parcselect_Label_Debug' ) ) {
				Parcselect_Label_Debug::error( $order_id, 'pdf_endpoint.legacy_path_traversal_blocked', array(
					'path' => $legacy_path,
				) );
			}
		}

		// ── Brak PDF ─────────────────────────────────────────────────────────
		if ( class_exists( 'Parcselect_Label_Debug' ) ) {
			Parcselect_Label_Debug::error( $order_id, 'pdf_endpoint.not_found', array(
				'has_legacy_meta' => '' !== $legacy_path,
			) );
		}
		status_header( 404 );
		header( 'Content-Type: text/plain; charset=utf-8' );
		echo esc_html__( 'Label PDF is not available.', 'parcel-selector' );
		exit;
	}

	/**
	 * Strumieniuje bajty PDF z nagłówkami i kończy żądanie.
	 *
	 * @param string $pdf         Surowe bajty PDF.
	 * @param string $filename    Nazwa pliku dla Content-Disposition.
	 * @param string $disposition 'inline' | 'attachment'.
	 * @param string $source      Etykieta źródła do nagłówka X-PS-Label-Source.
	 */
	private static function stream_pdf( $pdf, $filename, $disposition, $source ) {
		while ( ob_get_level() ) {
			ob_end_clean();
		}
		nocache_headers();
		header( 'Content-Type: application/pdf' );
		header( 'Content-Disposition: ' . $disposition . '; filename="' . $filename . '"' );
		header( 'Content-Length: ' . strlen( $pdf ) );
		header( 'X-Content-Type-Options: nosniff' );
		header( 'Content-Security-Policy: default-src \'none\'' );
		header( 'X-PS-Label-Source: ' . $source );
		echo $pdf; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- binarny PDF, nie HTML.
		exit;
	}

	/**
	 * Buduje bezpieczną nazwę pliku PDF dla danego order.
	 *
	 * @param WC_Order $order
	 * @return string
	 */
	private static function filename_for( $order ) {
		$order_id = $order->get_id();
		$tracking = (string) $order->get_meta( '_ps_label_tracking_number', true );
		$suffix   = $tracking ? '-' . substr( preg_replace( '/[^A-Za-z0-9]/', '', $tracking ), -8 ) : '';
		return 'etykieta-' . absint( $order_id ) . $suffix . '.pdf';
	}
}
