<?php
namespace Pay4All\Pro\Integrations\Age;

use Pay4All\Age\Frontend\PanelRenderer;
use Pay4All\Age\Services\Config;
use Pay4All\Age\Services\Kyc;

// phpcs:disable WordPress.WP.I18n.TextDomainMismatch -- This bridge intentionally
// reuses pay4all-age's text domain so the merchant sees a single consistent set
// of translated strings on the WooCommerce checkout (the free plugin ships the
// .mo files, this pro companion just injects the same texts on WC surfaces).

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

/**
 * Renders the KYC panel and the (optional) OR-choice panel into both
 * classic and Blocks WooCommerce checkout pages, and blocks order
 * validation until every required slot is uploaded.
 *
 * Layout:
 *   - Main panel  : heading + description + QR code + slots. Rendered
 *                   at the top of the checkout on classic (before the
 *                   billing form) and injected as the first child of
 *                   the Blocks checkout container.
 *   - OR panel    : the "ID or selfies" radio widget. Rendered right
 *                   after the payment-method choice — classic hook
 *                   `woocommerce_review_order_after_payment`, Blocks
 *                   via a JS injection after the payment-method block.
 *
 * The choice widget lives in its own DOM node but JS keeps it in sync
 * with the slot visibility of the main panel (they share the same
 * `input[name="p4all_age_choice"]` name).
 */
final class Checkout {

	public static function register() : void {
		add_action( 'wp_enqueue_scripts', [ __CLASS__, 'enqueue' ] );

		// Classic checkout — a single panel above the form that carries
		// the OR-choice widget, the QR code, and the slots.
		add_action( 'woocommerce_before_checkout_form',      [ __CLASS__, 'render_main_classic' ], 5 );
		add_action( 'woocommerce_after_checkout_validation', [ __CLASS__, 'validate' ], 10, 2 );
		// The main panel renders OUTSIDE the checkout <form> (via
		// `woocommerce_before_checkout_form`), so its `p4all_age_kyc_
		// token` hidden input is never POSTed. This hook fires INSIDE
		// the form, right after the customer details block, so the
		// token reliably reaches the server on submit.
		add_action( 'woocommerce_checkout_after_customer_details', [ __CLASS__, 'render_hidden_token_in_form' ] );

		// Blocks checkout — templates in wp_footer, JS injects them into
		// the block tree.
		add_action( 'wp_footer', [ __CLASS__, 'render_footer_templates' ] );
		add_action( 'woocommerce_store_api_checkout_update_order_from_request', [ __CLASS__, 'validate_store_api' ], 10, 2 );
	}

	public static function enqueue() : void {
		// Do NOT guard on is_checkout() — Elementor Pro (and Divi's
		// checkout module, and some FSE templates) render the checkout
		// via a widget/block that `is_checkout()` may not detect while
		// `wp_enqueue_scripts` fires. Guarding only on cart contents is
		// enough : the JS is inert unless it finds `.p4all-age-panel`
		// in the DOM, and the CSS only styles our own classes so it
		// can't leak into the rest of the page.
		if ( is_admin() ) { return; }
		if ( ! function_exists( 'WC' ) || ! WC()->cart ) { return; }
		if ( ! WooCart::cart_needs_kyc() ) { return; }

		wp_enqueue_style(
			'p4all-age-checkout',
			P4ALL_AGE_URL . 'assets/public/css/checkout.css',
			[],
			P4ALL_AGE_VERSION
		);
		wp_enqueue_script(
			'p4all-age-qr',
			P4ALL_AGE_URL . 'assets/public/js/qr.js',
			[],
			P4ALL_AGE_VERSION,
			true
		);
		wp_enqueue_script(
			'p4all-age-checkout',
			P4ALL_AGE_URL . 'assets/public/js/checkout.js',
			[ 'p4all-age-qr' ],
			P4ALL_AGE_VERSION,
			true
		);
		wp_localize_script( 'p4all-age-checkout', 'p4allAgeI18n', [
			'sent'       => __( 'Envoyé', 'pay4all-age' ),
			'sending'    => __( 'Envoi en cours…', 'pay4all-age' ),
			'uploadFail' => __( 'Échec de l\'envoi. Réessayez.', 'pay4all-age' ),
			'waiting'    => Config::text( 'waiting' ),
		] );
	}

	private static function is_checkout_context() : bool {
		if ( function_exists( 'is_checkout' ) && is_checkout() ) { return true; }
		if ( function_exists( 'has_block' ) ) {
			global $post;
			if ( $post && has_block( 'woocommerce/checkout', $post ) ) { return true; }
		}
		return false;
	}

	// ---------------------------------------------------------------
	// Classic-checkout render hooks
	// ---------------------------------------------------------------

	public static function render_main_classic() : void {
		if ( ! WooCart::cart_needs_kyc() ) { return; }
		self::render_main_html();
	}

	/**
	 * Emits a hidden `p4all_age_kyc_token` inside the checkout <form> so
	 * `Kyc::locate_token()` finds it on submit — the main panel is above
	 * the form and its own hidden input is never POSTed.
	 */
	public static function render_hidden_token_in_form() : void {
		if ( ! WooCart::cart_needs_kyc() ) { return; }
		$token = Kyc::ensure_session_token();
		if ( $token === '' ) { return; }
		echo '<input type="hidden" name="p4all_age_kyc_token" value="' . esc_attr( $token ) . '">';
	}

	public static function render_choice_classic() : void {
		if ( ! WooCart::cart_needs_kyc() ) { return; }
		self::render_choice_html();
	}

	// ---------------------------------------------------------------
	// Blocks-checkout render hooks
	// ---------------------------------------------------------------

	/**
	 * Emits hidden templates in wp_footer that the JS bootstrapper
	 * relocates into the Blocks checkout tree. `checkout.js` uses a
	 * MutationObserver so we don't race the React hydration order.
	 */
	public static function render_footer_templates() : void {
		// Emits an HTML comment on every wp_footer call so we can trace
		// the exact reason nothing appears on the checkout. Search
		// "p4all-age:" in the page source.
		echo "\n<!-- p4all-age: wp_footer fired at " . esc_html( gmdate( 'H:i:s' ) ) . " -->\n";

		if ( is_admin() ) {
			echo "<!-- p4all-age: SKIP is_admin -->\n";
			return;
		}
		if ( ! function_exists( 'WC' ) || ! WC()->cart ) {
			echo "<!-- p4all-age: SKIP no WC()->cart -->\n";
			return;
		}
		$items_meta = [];
		foreach ( WC()->cart->get_cart() as $item ) {
			$pid = 0;
			if ( is_object( $item['data'] ?? null ) && method_exists( $item['data'], 'get_id' ) ) {
				$pid = (int) $item['data']->get_id();
			}
			$items_meta[] = $pid . '=' . (string) get_post_meta( $pid, '_pay4all_age_required', true );
		}
		echo '<!-- p4all-age: cart items ' . esc_html( implode( ' ', $items_meta ) ) . " -->\n";

		if ( ! WooCart::cart_needs_kyc() ) {
			echo "<!-- p4all-age: SKIP cart_needs_kyc returned FALSE -->\n";
			return;
		}
		// An inline panel already rendered on this page (classic hook
		// fired for Elementor Pro Checkout widget, pay4all-form
		// shortcode, register-form opt-in, etc). Emitting a second copy
		// here would duplicate every `id` and break every `<label for>`
		// downstream.
		if ( PanelRenderer::inline_panel_rendered() ) {
			echo "<!-- p4all-age: SKIP inline panel already rendered -->\n";
			return;
		}
		echo "<!-- p4all-age: EMITTING template -->\n";
		?>
		<div id="p4all-age-main-template" data-p4all-age-template="main" style="display:none;">
			<?php self::render_main_html(); ?>
		</div>
		<?php
	}

	// ---------------------------------------------------------------
	// HTML renderers
	// ---------------------------------------------------------------

	/**
	 * Emits the main KYC panel by delegating to the shared PanelRenderer
	 * that lives in pay4all-age — same markup used by pay4all-form's
	 * shortcode integration, so the CSS and JS have a single source.
	 */
	public static function render_main_html() : void {
		// Belt-and-suspenders : re-enqueue assets here in case the normal
		// `wp_enqueue_scripts` hook was too early (e.g. Elementor Pro
		// checkout widget on a page where `is_checkout()` was false at
		// hook time). wp_enqueue_* is idempotent — a duplicate call is
		// a no-op — but this guarantees the CSS + JS reach the footer.
		self::enqueue();
		PanelRenderer::render_main_html();
	}

	/**
	 * OR-choice panel — separate small card containing only the radio
	 * widget. Skipped entirely when the plugin isn't in "or" mode or
	 * only one type is required (no choice to offer).
	 */
	private static function render_choice_html() : void {
		$cfg = Config::required_slots();
		if ( $cfg['mode'] !== 'or' || ! $cfg['required_id'] || ! $cfg['required_selfies'] ) { return; }

		$choice_prompt = Config::text( 'choice_prompt' );
		$choice_labels = [
			'id'      => Config::text( 'choice_id' ),
			'selfies' => Config::text( 'choice_selfies' ),
		];
		// Radios follow the merchant's drag-drop order (slot_order). The
		// first entry gets `checked` so the pre-selected option matches
		// what the customer visually sees on top of the list.
		$order = $cfg['slot_order'] ?? \Pay4All\Age\Services\Config::SLOT_GROUPS;
		$order = array_values( array_intersect( (array) $order, array_keys( $choice_labels ) ) );
		if ( empty( $order ) ) { $order = array_keys( $choice_labels ); }
		$default = $order[0];
		?>
		<div class="p4all-age-choice-panel">
			<p class="p4all-age-choice-prompt"><?php echo esc_html( $choice_prompt ); ?></p>
			<?php foreach ( $order as $group ) : ?>
				<label class="p4all-age-choice-option">
					<input type="radio" name="p4all_age_choice" value="<?php echo esc_attr( $group ); ?>" <?php checked( $default, $group ); ?>>
					<span><?php echo esc_html( $choice_labels[ $group ] ); ?></span>
				</label>
			<?php endforeach; ?>
		</div>
		<?php
	}

	// ---------------------------------------------------------------
	// Server-side validation
	// ---------------------------------------------------------------

	/**
	 * Classic-checkout validator.
	 *
	 * @param array     $data   Posted checkout data.
	 * @param \WP_Error $errors WC error bag we append to.
	 */
	public static function validate( $data, $errors ) : void {
		if ( ! WooCart::cart_needs_kyc() ) { return; }
		if ( ! self::current_session_satisfied() ) {
			$errors->add( 'p4all_age_missing', Config::text( 'error_missing' ) );
		}
	}

	/**
	 * Store-API validator (Blocks). Throws a RouteException which the
	 * block converts into a 400 response the block UI surfaces.
	 *
	 * @param \WC_Order        $order   Draft order about to be placed.
	 * @param \WP_REST_Request $request Incoming Store API request.
	 */
	public static function validate_store_api( $order, $request ) : void {
		if ( ! WooCart::cart_needs_kyc() ) { return; }
		if ( self::current_session_satisfied() ) { return; }

		if ( class_exists( '\\Automattic\\WooCommerce\\StoreApi\\Exceptions\\RouteException' ) ) {
			throw new \Automattic\WooCommerce\StoreApi\Exceptions\RouteException(
				'p4all_age_missing',
				esc_html( Config::text( 'error_missing' ) ),
				400
			);
		}
	}

	private static function current_session_satisfied() : bool {
		// Nonce verification is not applicable : the KYC session token is
		// verified by pattern match + transient lookup (see Kyc::get_session).
		// The submitted-form path is the WC checkout POST which carries WC's
		// own nonce already, and the cookie fallback is the standard session
		// cookie authenticated by the transient's cryptographic key. Both
		// $_POST and $_COOKIE lookups below are read-only checks — no side
		// effects until Kyc::get_session() validates the token.
		// phpcs:disable WordPress.Security.NonceVerification.Missing -- token authenticated by transient lookup downstream.
		// phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- pattern-matched against ^[a-f0-9]{12}$ before use.
		// phpcs:disable WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- pattern-matched against ^[a-f0-9]{12}$ before use.
		$token = '';
		if ( isset( $_POST['p4all_age_kyc_token'] ) ) {
			$token = sanitize_text_field( wp_unslash( (string) $_POST['p4all_age_kyc_token'] ) );
		}
		if ( ! preg_match( '/^[a-f0-9]{12}$/', $token ) ) {
			$token = isset( $_COOKIE[ Kyc::SESSION_COOKIE ] ) ? sanitize_text_field( wp_unslash( (string) $_COOKIE[ Kyc::SESSION_COOKIE ] ) ) : '';
		}
		// phpcs:enable WordPress.Security.NonceVerification.Missing
		// phpcs:enable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
		// phpcs:enable WordPress.Security.ValidatedSanitizedInput.MissingUnslash
		$session = preg_match( '/^[a-f0-9]{12}$/', $token ) ? Kyc::get_session( $token ) : null;
		$slots   = is_array( $session ) ? (array) ( $session['slots'] ?? [] ) : [];
		$choice  = is_array( $session ) && isset( $session['choice'] )
			&& in_array( $session['choice'], [ 'id', 'selfies' ], true )
			? (string) $session['choice'] : null;
		return Kyc::is_satisfied( $slots, $choice );
	}
}
