<?php

namespace SugarCalendarBookings\Admin\Settings;

use SugarCalendarBookings\Antispam\AntispamService;
use SugarCalendarBookings\Antispam\Captcha\CaptchaVerifierRegistry;
use SugarCalendarBookings\Antispam\Captcha\Settings\AbstractCaptchaSettings;
use SugarCalendarBookings\Options;
use SugarCalendarBookings\Plugin;
use SugarCalendarBookings\Support\Admin\AbstractTabPage;
use SugarCalendarBookings\Support\Admin\Notices;
use SugarCalendarBookings\Support\Helpers\Helpers;
use SugarCalendarBookings\Support\Helpers\UI;

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

/**
 * Anti-Spam Settings tab.
 *
 * Thin host for the captcha-settings layer — each captcha category's settings
 * class (under `Antispam\Captcha\Settings`) owns card metadata, panel
 * rendering, validation rules, post-data sanitize, and the localized JS data
 * block. Adding a captcha category = new settings class + one registry entry.
 *
 * @since 1.5.0
 */
class AntispamSettingsTab extends AbstractTabPage {

	/**
	 * UI category key for the "no captcha" card.
	 *
	 * Unlike `recaptcha` / `turnstile`, the None card has no captcha provider
	 * behind it — it is the disabled state. Handled inline by this tab rather
	 * than as a captcha settings class.
	 *
	 * @since 1.5.0
	 */
	private const CATEGORY_NONE = 'none';

	/**
	 * Page title.
	 *
	 * @since 1.5.0
	 *
	 * @return string
	 */
	public static function get_title() {

		return esc_html__( 'Anti-Spam', 'sugar-calendar-bookings-scheduling-appointments-lite' );
	}

	/**
	 * Page slug.
	 *
	 * @since 1.5.0
	 *
	 * @return string
	 */
	public static function get_slug() {

		return 'settings';
	}

	/**
	 * Page view slug.
	 *
	 * @since 1.5.0
	 *
	 * @return string
	 */
	public static function get_view_slug() {

		return 'antispam';
	}

	/**
	 * Priority in menu position.
	 *
	 * Sits between Integrations (50)/ImportExport (50) and Misc (60).
	 *
	 * @since 1.5.0
	 *
	 * @return int
	 */
	public static function get_priority() {

		return 55;
	}

	/**
	 * Display page content.
	 *
	 * @since 1.5.0
	 */
	public function display_content() { // phpcs:ignore Generic.Metrics.CyclomaticComplexity.TooHigh

		$options       = Plugin::get( Options::class );
		$stored_method = (string) $options->get( 'antispam.method' );
		$active        = CaptchaVerifierRegistry::resolve_settings_for_method( $stored_method );
		$category      = $active ? $active->get_key() : self::CATEGORY_NONE;

		ob_start();

			UI::heading(
				[
					'title'       => esc_html__( 'Anti-Spam Configuration', 'sugar-calendar-bookings-scheduling-appointments-lite' ),
					'description' => esc_html__( 'Protect your booking form from spam and abuse while letting real people pass through with ease.', 'sugar-calendar-bookings-scheduling-appointments-lite' ),
				]
			);

			$this->render_method_cards( $category, $stored_method );

			foreach ( CaptchaVerifierRegistry::all_settings() as $settings ) {
				$key = $settings->get_key();

				printf(
					'<div id="scb-antispam-panel-%1$s" class="scb-antispam-card-content %2$s">',
					esc_attr( $key ),
					esc_attr( $category === $key ? '' : 'scb-antispam-hide' )
				);

				// $html is captured output from UI::* helpers which escape their own input.
				echo $settings->render_panel( $stored_method ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped

				echo '</div>';
			}

			printf(
				'<div id="scb-antispam-panel-%1$s" class="scb-antispam-card-content %2$s"></div>',
				esc_attr( self::CATEGORY_NONE ),
				esc_attr( $category === self::CATEGORY_NONE ? '' : 'scb-antispam-hide' )
			);

			// Shared section (No-Conflict + Preview).
			printf(
				'<div id="scb-antispam-shared" class="%1$s">',
				$category === self::CATEGORY_NONE ? 'scb-antispam-hide' : ''
			);

				UI::toggle_control(
					[
						'id'          => 'antispam_no_conflict',
						'name'        => [ 'antispam', 'no_conflict' ],
						'value'       => $options->get( 'antispam.no_conflict' ),
						'label'       => esc_html__( 'No-Conflict Mode', 'sugar-calendar-bookings-scheduling-appointments-lite' ),
						'description' => esc_html__( 'Forcefully remove other CAPTCHA occurrences in order to prevent conflicts. Only enable this option if your site is having compatibility issues or instructed by support.', 'sugar-calendar-bookings-scheduling-appointments-lite' ),
					]
				);

				$has_keys       = $this->has_complete_keys( $category );
				$preview_hidden = $this->is_preview_hidden( $category );

				if ( ! $has_keys ) {
					$preview_desc = esc_html__( 'Please save settings to generate a preview of your CAPTCHA here.', 'sugar-calendar-bookings-scheduling-appointments-lite' );
				} elseif ( $active !== null ) {
					$preview_desc = $active->get_preview_description();
				} else {
					$preview_desc = '';
				}

				$preview_target_classes = 'scb-antispam-preview-widget' . ( $has_keys ? '' : ' scb-antispam-hide' );
				$preview_content        = sprintf(
					'<div id="scb-antispam-preview" class="%1$s"></div>',
					esc_attr( $preview_target_classes )
				);
				$preview_content       .= sprintf(
					'<p id="scb-antispam-preview-description" class="desc">%1$s</p>',
					esc_html( $preview_desc )
				);

				UI::field_wrapper(
					[
						'id'    => 'antispam-preview',
						'type'  => 'antispam-preview',
						'label' => esc_html__( 'Preview', 'sugar-calendar-bookings-scheduling-appointments-lite' ),
						'class' => $preview_hidden ? 'scb-antispam-hide' : '',
					],
					$preview_content
				);

			echo '</div>'; // End shared section.

		UI::form_wrapper( ob_get_clean() );
	}

	/**
	 * Get post data processing rules.
	 *
	 * @since 1.5.0
	 *
	 * @return array
	 */
	public function get_post_data_processing_rules() {

		$allowed_methods = array_merge( [ self::CATEGORY_NONE ], array_keys( CaptchaVerifierRegistry::all() ) );

		$rules = [
			'antispam.method'      => 'required|string|in:' . implode( ',', $allowed_methods ),
			'antispam.no_conflict' => 'form_boolean',
		];

		$captcha_settings = CaptchaVerifierRegistry::all_settings();

		foreach ( $captcha_settings as $settings ) {
			$rules = array_merge( $rules, $settings->get_validation_rules() );
		}

		return $rules;
	}

	/**
	 * Handle POST requests.
	 *
	 * @since 1.5.0
	 *
	 * @param array $post_data Post data.
	 */
	public function handle_post( $post_data = [] ) {

		if ( isset( $post_data['antispam'] ) ) {
			$antispam = (array) $post_data['antispam'];

			foreach ( CaptchaVerifierRegistry::all_settings() as $settings ) {
				$antispam = $settings->sanitize_post_data( $antispam );
			}

			$post_data['antispam'] = $antispam;
		}

		Plugin::get( Options::class )->update( $post_data );

		Plugin::get( Notices::class )->success(
			esc_html__( 'Settings successfully saved.', 'sugar-calendar-bookings-scheduling-appointments-lite' )
		);
	}

	/**
	 * Render the method selection cards.
	 *
	 * The hidden flat-method input is seeded with the saved value (or, when an
	 * active category exists but the stored method doesn't belong to it, that
	 * category's default); client-side JS rewrites it on card / sub-radio
	 * change.
	 *
	 * @since 1.5.0
	 *
	 * @param string $current_category Currently selected UI category.
	 * @param string $stored_method    Currently saved flat method.
	 */
	private function render_method_cards( $current_category, $stored_method ) {

		$cards   = $this->get_cards();
		$current = array_key_exists( $current_category, $cards ) ? $current_category : self::CATEGORY_NONE;

		echo '<div class="scb-antispam-method-cards">';

		foreach ( $cards as $key => $data ) {
			printf(
				'<div class="scb-antispam-method-card %1$s" data-method="%2$s">',
				$current === $key ? 'selected' : '',
				esc_attr( $key )
			);

			echo '<div class="scb-antispam-method-card-icon">';
				printf(
					'<img src="%1$s" alt="%2$s" />',
					esc_url( $data['icon'] ),
					esc_attr( $data['title'] )
				);
			echo '</div>';

			printf(
				'<div class="scb-antispam-method-card-title">%1$s</div>',
				esc_html( $data['title'] )
			);

			echo '</div>';
		}

		$hidden_value = $this->resolve_hidden_method_value( $current, $stored_method );

		// Hidden input that holds the flat method for form submission.
		printf(
			'<input type="hidden" name="sugar-calendar-bookings[antispam][method]" value="%1$s" />',
			esc_attr( $hidden_value )
		);

		echo '</div>';

		// Method description block (swapped client-side on card click).
		printf(
			'<div class="scb-antispam-method-description">%1$s</div>',
			wp_kses_post( $cards[ $current ]['description'] )
		);
	}

	/**
	 * Assemble card metadata for every available category plus the None card.
	 *
	 * @since 1.5.0
	 *
	 * @return array<string, array> Keyed by category, ordered: settings classes first, None last.
	 */
	private function get_cards(): array {

		$cards = [];

		foreach ( CaptchaVerifierRegistry::all_settings() as $settings ) {
			$cards[ $settings->get_key() ] = $settings->get_card();
		}

		$cards[ self::CATEGORY_NONE ] = $this->get_none_card();

		return $cards;
	}

	/**
	 * Card metadata for the None (no captcha) option.
	 *
	 * @since 1.5.0
	 *
	 * @return array
	 */
	private function get_none_card(): array {

		return [
			'title'       => esc_html__( 'None', 'sugar-calendar-bookings-scheduling-appointments-lite' ),
			'icon'        => SCBOOKINGS_PLUGIN_URL . 'assets/images/icons/settings-captcha-none.svg',
			'description' => '<p>' . wp_kses(
				sprintf(
					/* translators: %1$s - link to the SCB anti-spam documentation. */
					__( 'Not sure which service is right for you? <a href="%1$s" target="_blank" rel="noopener">Check out our comparison</a> for more details.', 'sugar-calendar-bookings-scheduling-appointments-lite' ),
					esc_url(
						Helpers::get_utm_url(
							'https://sugarcalendar.com/docs/bookings/enabling-spam-protection-on-booking-forms/',
							[
								'content' => 'documentation',
								'medium'  => 'plugin-settings-antispam',
							]
						)
					)
				),
				AbstractCaptchaSettings::allowed_link_html()
			) . '</p>',
		];
	}

	/**
	 * Resolve the value for the hidden flat-method input.
	 *
	 * If the current category owns the stored method, use it as-is. Otherwise
	 * seed with the category's default method (or 'none' for the None card).
	 *
	 * @since 1.5.0
	 *
	 * @param string $current_category Currently selected UI category.
	 * @param string $stored_method    Currently saved flat method.
	 *
	 * @return string
	 */
	private function resolve_hidden_method_value( string $current_category, string $stored_method ): string {

		if ( $current_category === self::CATEGORY_NONE ) {
			return self::CATEGORY_NONE;
		}

		$settings = CaptchaVerifierRegistry::all_settings()[ $current_category ] ?? null;

		if ( $settings === null ) {
			return self::CATEGORY_NONE;
		}

		if ( $settings->owns_method( $stored_method ) ) {
			return $stored_method;
		}

		return $settings->default_method_key();
	}

	/**
	 * Register hooks.
	 *
	 * @since 1.5.0
	 */
	public function hooks() {

		parent::hooks();

		add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ] );
	}

	/**
	 * Register and conditionally enqueue third-party captcha API scripts.
	 *
	 * @since 1.5.0
	 */
	public function enqueue_assets() {

		$class = CaptchaVerifierRegistry::resolve_active_class();

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

		$method   = (string) Plugin::get( Options::class )->get( 'antispam.method' );
		$settings = CaptchaVerifierRegistry::resolve_settings_for_method( $method );
		$category = $settings ? $settings->get_key() : self::CATEGORY_NONE;

		// Skip the third-party API load when keys aren't fully configured —
		// there's nothing to render, and v3 would build a malformed request URL
		// from an empty site_key.
		if ( ! $this->has_complete_keys( $category ) ) {
			return;
		}

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

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

		// Force `render=explicit` on the admin preview script even when the
		// active provider (v3) would otherwise return its site key. The site-
		// key form auto-renders a floating badge at script-load time, which
		// fights with the inline preview render in `initGrecaptchaPreview()`.
		// Explicit-only render keeps a single badge inside the preview box.
		//
		// 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_enqueue_script(
			$class::SCRIPT_HANDLE,
			add_query_arg(
				[
					'onload' => 'scbAntispamOnCaptchaLoad',
					'render' => 'explicit',
				],
				$class::SCRIPT_API_URL
			),
			[],
			SCBOOKINGS_PLUGIN_VERSION,
			true
		);
		// phpcs:enable PluginCheck.CodeAnalysis.EnqueuedResourceOffloading.OffloadedContent
	}

	/**
	 * Whether the active captcha category has both site and secret keys saved.
	 *
	 * Returns true only when the saved category is a real captcha provider
	 * (not None) and both keys are non-empty. Drives the preview UI state and
	 * the captcha-API enqueue — neither makes sense without a complete key
	 * pair.
	 *
	 * @since 1.5.0
	 *
	 * @param string $category Active UI category.
	 *
	 * @return bool
	 */
	private function has_complete_keys( $category ) {

		$class = CaptchaVerifierRegistry::class_for_category( (string) $category );

		return $class !== null && ( new $class() )->has_complete_keys();
	}

	/**
	 * Whether the captcha preview row should be hidden for the active method.
	 *
	 * True when no captcha is active, or when the active verifier reports it
	 * has no visible widget (e.g. reCAPTCHA V3's silent score check).
	 *
	 * @since 1.5.0
	 *
	 * @param string $category Active UI category.
	 *
	 * @return bool
	 */
	private function is_preview_hidden( string $category ): bool {

		if ( $category === self::CATEGORY_NONE ) {
			return true;
		}

		$active_verifier_class = CaptchaVerifierRegistry::resolve_active_class();

		return $active_verifier_class !== null && ! $active_verifier_class::HAS_VISIBLE_WIDGET;
	}

	/**
	 * Get page script data localized to the JS entry.
	 *
	 * Secret keys are intentionally excluded — only public site keys are exposed.
	 *
	 * @since 1.5.0
	 *
	 * @return array
	 */
	public function get_script_data() {

		$cards        = $this->get_cards();
		$descriptions = [];

		foreach ( $cards as $key => $data ) {
			$descriptions[ $key ] = $data['description'];
		}

		$antispam = [
			'preview_save_instruction' => esc_html__(
				'Please save settings to generate a preview of your CAPTCHA here.',
				'sugar-calendar-bookings-scheduling-appointments-lite'
			),
			'method_descriptions'      => $descriptions,
		];

		$captcha_settings = CaptchaVerifierRegistry::all_settings();

		foreach ( $captcha_settings as $settings ) {
			$antispam[ $settings->get_key() ] = $settings->get_admin_script_data();
		}

		return [
			'antispam' => $antispam,
		];
	}
}
