<?php

namespace SugarCalendarBookings\BookingForm;

use SugarCalendarBookings\Antispam\AntispamService;
use SugarCalendarBookings\Antispam\Captcha\CaptchaVerifierRegistry;
use SugarCalendarBookings\Admin\Assets;
use SugarCalendarBookings\Plugin;
use SugarCalendarBookings\Support\Bootable;

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

/**
 * Booking Form Assets.
 *
 * Centralized registration and localization of booking form frontend assets.
 * This ensures scripts are registered early with proper localization data before
 * any chunks are loaded, regardless of whether the form is used via block, shortcode, or Elementor.
 *
 * @since 1.0.1
 * @since 1.4.0 Theme compatibility stylesheets added.
 */
class BookingFormAssets implements Bootable {

	/**
	 * Theme compatibility stylesheets to load with the booking form.
	 *
	 * Populated by check_compatibility_styles() based on the active theme.
	 * Each entry maps a handle to its CSS file path (relative to assets/dist/).
	 * These are registered as dependencies of the main booking form style so
	 * WordPress loads them automatically.
	 *
	 * @since 1.4.0
	 *
	 * @var array<string, string>
	 */
	private array $booking_form_deps = [];

	/**
	 * Check active theme and populate compatibility styles.
	 *
	 * @since 1.4.0
	 */
	public function init(): void {

		$this->check_compatibility_styles();
	}

	/**
	 * Register hooks.
	 *
	 * @since 1.0.1
	 */
	public function hooks(): void {

		// Register scripts early with localization before they're enqueued.
		add_action( 'wp_enqueue_scripts', [ $this, 'register_frontend_assets' ], 5 );

		// Inject compat styles as dependencies of the booking form style.
		add_action( 'wp_enqueue_scripts', [ $this, 'inject_compatibility_deps' ], 6 );

		// Run no-conflict dequeue late, after all enqueues, before printing.
		add_action( 'wp_print_scripts', [ $this, 'dequeue_conflicting_captcha_scripts' ], 1 );
	}

	/**
	 * Detect active theme and add any needed compatibility stylesheets
	 * as dependencies of the booking form style.
	 *
	 * @since 1.4.0
	 */
	private function check_compatibility_styles(): void {

		$theme_compat = [
			'hello-elementor' => 'styles/compatibility/compat-hello-elementor.css',
			'astra'           => 'styles/compatibility/compat-astra.css',
		];

		$active_theme = get_template();

		if ( isset( $theme_compat[ $active_theme ] ) ) {
			$this->booking_form_deps[ 'scbookings-compat-' . $active_theme ] = $theme_compat[ $active_theme ];
		}
	}

	/**
	 * Register frontend assets for the booking form.
	 *
	 * This registers scripts early with localization to ensure the data is available
	 * before any chunks are loaded. Scripts are registered but not enqueued - they will
	 * only be enqueued when the form is actually used (via block, shortcode, or Elementor).
	 *
	 * @since 1.0.1
	 */
	public function register_frontend_assets(): void {

		// Register React JSX Runtime if not already registered.
		if ( ! wp_script_is( 'react-jsx-runtime', 'registered' ) ) {
			wp_register_script(
				'react-jsx-runtime',
				SCBOOKINGS_PLUGIN_URL . 'assets/lib/react-jsx-runtime.js',
				[ 'react' ],
				'18.3.1',
				true
			);
		}

		// Register the booking form script (with manifest deps + webpack runtime as a dep).
		$this->register_booking_form_script();

		// Always localize script data, even if the script was already registered by block.json.
		// This ensures the data is available before any chunks load.
		wp_localize_script(
			'scbookings-booking-form-script',
			'sugar_calendar_bookings',
			Plugin::get( BookingFormService::class )->get_script_data()
		);

		// Set script translations for i18n support.
		wp_set_script_translations(
			'scbookings-booking-form-script',
			'sugar-calendar-bookings-scheduling-appointments-lite',
			SCBOOKINGS_PLUGIN_PATH . 'assets/languages'
		);

		// Register theme compatibility stylesheets as dependencies.
		foreach ( $this->booking_form_deps as $handle => $file ) {
			wp_register_style(
				$handle,
				SCBOOKINGS_PLUGIN_URL . 'assets/dist/' . $file,
				[],
				SCBOOKINGS_PLUGIN_VERSION
			);
		}

		// Register the booking form style if not already registered.
		if ( ! wp_style_is( 'scbookings-booking-form-style', 'registered' ) ) {
			wp_register_style(
				'scbookings-booking-form-style',
				SCBOOKINGS_PLUGIN_URL . 'assets/dist/booking-form/styles.css',
				array_keys( $this->booking_form_deps ),
				SCBOOKINGS_PLUGIN_VERSION
			);
		}

		// Register the third-party captcha script that matches the saved provider.
		$this->register_antispam_third_party_script();
	}

	/**
	 * Register the third-party captcha script that matches the saved provider.
	 *
	 * Registered (not enqueued) early so each rendering surface (Block, Shortcode,
	 * Elementor) can enqueue the same handle on demand. No-op when antispam is
	 * disabled.
	 *
	 * Also attaches an inline bootstrap that defines the global onload callback
	 * BEFORE the third-party script tag runs. This is required because the
	 * booking-form React bundle lazy-loads the PaymentDetails step (and therefore
	 * the AntispamCaptcha component), so the JS-side bootstrap may not have run
	 * by the time api.js fires its onload.
	 *
	 * @since 1.5.0
	 */
	private function register_antispam_third_party_script(): void {

		$handle = Plugin::get( AntispamService::class )->get_active_script_handle();

		if ( $handle === null ) {
			return;
		}

		$active = Plugin::get( AntispamService::class )->get_active_provider();
		$class  = get_class( $active );

		// Provider-mandated origin: site_key is bound to api.js host
		// server-side, so self-hosting breaks verification.
		// phpcs:disable PluginCheck.CodeAnalysis.EnqueuedResourceOffloading.OffloadedContent
		wp_register_script(
			$handle,
			add_query_arg(
				[
					'onload' => 'scbookingsAntispamOnCaptchaLoad',
					'render' => $active->get_script_render_arg(),
				],
				$class::SCRIPT_API_URL
			),
			[],
			SCBOOKINGS_PLUGIN_VERSION,
			true
		);
		// phpcs:enable PluginCheck.CodeAnalysis.EnqueuedResourceOffloading.OffloadedContent

		wp_add_inline_script(
			$handle,
			$this->get_antispam_bootstrap_script(),
			'before'
		);
	}

	/**
	 * Return the inline JS that defines the global captcha onload callback.
	 *
	 * Defined here in PHP so the global is always available when the third-party
	 * script's onload fires — independent of how (or when) the React bundle is
	 * evaluated.
	 *
	 * @since 1.5.0
	 *
	 * @return string
	 */
	private function get_antispam_bootstrap_script(): string {

		return 'window.scbookingsAntispamOnCaptchaLoad = window.scbookingsAntispamOnCaptchaLoad || function () { window.scbookingsAntispamReady = true; (window.scbookingsAntispamPending || []).forEach(function (fn) { fn(); }); window.scbookingsAntispamPending = []; };';
	}

	/**
	 * Enqueue the matching third-party captcha script.
	 *
	 * Single source of truth for which captcha provider to load. Called by each
	 * rendering surface (Block / Shortcode / Elementor) alongside the booking
	 * form script enqueue. No-op when antispam is disabled.
	 *
	 * @since 1.5.0
	 */
	public function enqueue_antispam_third_party_script(): void {

		$handle = Plugin::get( AntispamService::class )->get_active_script_handle();

		if ( $handle === null ) {
			return;
		}

		wp_enqueue_script( $handle );
	}

	/**
	 * Register the booking-form script with manifest deps and the webpack runtime.
	 *
	 * @since 1.5.0
	 */
	private function register_booking_form_script(): void { // phpcs:ignore Generic.Metrics.CyclomaticComplexity.MaxExceeded

		$runtime        = Plugin::get( Assets::class )->get_asset_manifest( 'runtime' ) ?: [];
		$runtime_handle = $runtime['handle'] ?? '';

		if ( $runtime_handle !== '' && ! wp_script_is( $runtime_handle, 'registered' ) ) {
			wp_register_script(
				$runtime_handle,
				SCBOOKINGS_PLUGIN_URL . 'assets/dist/runtime.js',
				$runtime['dependencies'] ?? [],
				$runtime['version'] ?? SCBOOKINGS_PLUGIN_VERSION,
				true
			);
		}

		if ( wp_script_is( 'scbookings-booking-form-script', 'registered' ) ) {
			$script = wp_scripts()->registered['scbookings-booking-form-script'] ?? null;

			if ( $script && $runtime_handle !== '' && ! in_array( $runtime_handle, $script->deps, true ) ) {
				$script->deps[] = $runtime_handle;
			}

			return;
		}

		$frontend = Plugin::get( Assets::class )->get_asset_manifest( 'booking-form/frontend' ) ?: [];
		$deps     = $frontend['dependencies'] ?? [ 'wp-element', 'react-jsx-runtime' ];

		if ( $runtime_handle !== '' ) {
			$deps[] = $runtime_handle;
		}

		wp_register_script(
			'scbookings-booking-form-script',
			SCBOOKINGS_PLUGIN_URL . 'assets/dist/booking-form/frontend.js',
			$deps,
			$frontend['version'] ?? SCBOOKINGS_PLUGIN_VERSION,
			true
		);
	}

	/**
	 * Inject compatibility stylesheets as dependencies of the booking form style.
	 *
	 * Runs after register_frontend_assets to handle the case where block.json
	 * already registered the style handle during init (before we could set deps).
	 * Appends compat handles to the existing _WP_Dependency::$deps array.
	 *
	 * @since 1.4.0
	 */
	public function inject_compatibility_deps(): void {

		if ( empty( $this->booking_form_deps ) ) {
			return;
		}

		$style = wp_styles()->registered['scbookings-booking-form-style'] ?? null;

		if ( ! $style ) {
			return;
		}

		$new_deps = array_diff( array_keys( $this->booking_form_deps ), $style->deps );

		if ( ! empty( $new_deps ) ) {
			$style->deps = array_merge( $style->deps, $new_deps );
		}
	}

	/**
	 * Dequeue and deregister conflicting third-party captcha scripts.
	 *
	 * Direct port of sc-rsvp/src/Frontend.php::recaptcha_noconflict(). Runs only
	 * when:
	 *   - antispam is enabled and `no_conflict` is on
	 *   - the booking form script has been enqueued on this page (so we don't
	 *     touch unrelated pages)
	 * Skips own handles ('scbookings-' prefix). Bypassable via filter.
	 *
	 * @since 1.5.0
	 */
	public function dequeue_conflicting_captcha_scripts(): void { // phpcs:ignore Generic.Metrics.CyclomaticComplexity.MaxExceeded

		// Only run on pages that render the booking form.
		if ( ! wp_script_is( 'scbookings-booking-form-script', 'enqueued' ) ) {
			return;
		}

		$antispam = Plugin::get( AntispamService::class );

		if ( $antispam->get_active_provider() === null ) {
			return;
		}

		if ( ! $antispam->is_no_conflict_enabled() ) {
			return;
		}

		/**
		 * Filter to bypass the no-conflict dequeue.
		 *
		 * Returning true skips the dequeue entirely.
		 *
		 * @since 1.5.0
		 *
		 * @param bool $skip Whether to skip the dequeue.
		 */
		if ( (bool) apply_filters( 'sugar_calendar_bookings_booking_form_captcha_no_conflict', false ) ) { //phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName
			return;
		}

		$scripts        = wp_scripts();
		$conflict_hosts = [];

		foreach ( CaptchaVerifierRegistry::all() as $class ) {
			foreach ( $class::CONFLICT_HOSTS as $host ) {
				$conflict_hosts[ $host ] = true;
			}
		}

		$conflict_hosts = array_keys( $conflict_hosts );

		foreach ( $scripts->queue as $handle ) {

			if ( ! isset( $scripts->registered[ $handle ] ) ) {
				continue;
			}

			// Skip our own handles.
			if ( strpos( $handle, 'scbookings-' ) !== false ) {
				continue;
			}

			$src = (string) $scripts->registered[ $handle ]->src;

			foreach ( $conflict_hosts as $needle ) {
				if ( strpos( $src, $needle ) !== false ) {
					wp_dequeue_script( $handle );
					wp_deregister_script( $handle );
					break;
				}
			}
		}
	}
}
