<?php
/**
 * Donation Form Block
 *
 * Provides a block to display a donation form for the current fundraiser.
 *
 * @package AmbassadorHub\Fundraisers\Blocks\DonationForm
 */

namespace AmbassadorHub\Fundraisers\Blocks\DonationForm;

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

use AmbassadorHub\Connectors\ConnectorRegistry;
use AmbassadorHub\Donations\DonationsModule;
use AmbassadorHub\Fundraisers\Admin\Settings\SettingsManager;
use AmbassadorHub\Fundraisers\FundraisersHelpers;
use AmbassadorHub\Fundraisers\FundraisersModule;
use AmbassadorHub\Spam\SpamShield;
use AmbassadorHub\Support\RequestData;

/**
 * Donation Form block registration and rendering.
 */
final class Block {
	/**
	 * Initialize the block.
	 *
	 * @return void
	 */
	public static function init(): void {
		add_action( 'init', array( self::class, 'register_block' ) );

		// Form submission handlers (non-privileged and privileged for safety).
		add_action( 'admin_post_nopriv_ambh_submit_donation', array( self::class, 'handle_donation_submission' ) );
		add_action( 'admin_post_ambh_submit_donation', array( self::class, 'handle_donation_submission' ) );
	}

	/**
	 * Register the block.
	 *
	 * @return void
	 */
	public static function register_block(): void {
		// Register editor script.
		$handle    = 'ambh-donation-form-block';
		$block_dir = AMBASSADOR_HUB_PATH . 'src/Fundraisers/Blocks/DonationForm/';
		$block_url = AMBASSADOR_HUB_URL . 'src/Fundraisers/Blocks/DonationForm/';

		// Only register script if the file exists.
		$script_file = $block_dir . 'index.js';
		if ( file_exists( $script_file ) ) {
			wp_register_script(
				$handle,
				$block_url . 'index.js',
				array( 'wp-blocks', 'wp-i18n', 'wp-element', 'wp-server-side-render' ),
				filemtime( $script_file ),
				true
			);
		}

		// Register frontend styles.
		wp_register_style(
			'ambh-donation-form',
			$block_url . 'style.css',
			array(),
			AMBASSADOR_HUB_VERSION
		);

		$block_args = array(
			'style'           => 'ambh-donation-form',
			'render_callback' => array( self::class, 'render_block' ),
		);

		// Only include editor script if it was registered.
		if ( file_exists( $script_file ) ) {
			$block_args['editor_script'] = $handle;
		}

		register_block_type( $block_dir, $block_args );

		// Register shortcode alias.
		add_shortcode( 'ambh_donation_form', array( self::class, 'render_shortcode' ) );

		// Hook to localize script data and enqueue currency utility after block is rendered.
		add_action( 'wp_footer', array( self::class, 'localize_script' ) );
		add_action( 'wp_footer', array( self::class, 'enqueue_currency_assets' ) );
	}

	/**
	 * Localize script with configuration data.
	 * Called on wp_footer to ensure the block has been rendered.
	 *
	 * @return void
	 */
	/**
	 * Enqueue the currency input utility on fundraiser frontend pages.
	 *
	 * @return void
	 */
	public static function enqueue_currency_assets(): void {
		global $post;
		if ( ! isset( $post->ID ) || FundraisersModule::POST_TYPE !== get_post_type( $post ) ) {
			return;
		}
		SettingsManager::enqueue_currency_input_assets();
	}

	public static function localize_script(): void {
		global $post;

		// Only localize if we're on a fundraiser page.
		if ( ! isset( $post->ID ) || FundraisersModule::POST_TYPE !== get_post_type( $post ) ) {
			return;
		}

		// Localize script with API config.
		wp_localize_script(
			'ambassadorhub-donation-form-view',
			'ambhDonationFormConfig',
			array(
				'fundraiserId' => $post->ID,
				'apiUrl'       => rest_url( 'ambassadorhub/v1' ),
				'i18n'         => array(
					'submitText'    => __( 'Donate now', 'ambassadorhub' ),
					'submitting'    => __( 'Processing...', 'ambassadorhub' ),
					'success'       => __( 'Thank you for your donation! Payment processing will follow soon.', 'ambassadorhub' ),
					'errorRequired' => __( 'Please fill in all required fields.', 'ambassadorhub' ),
					'errorEmail'    => __( 'Please enter a valid email address.', 'ambassadorhub' ),
					/* translators: %s: minimum donation amount */
					'errorAmount'   => __( 'Please select or enter a valid donation amount (minimum: %s).', 'ambassadorhub' ),
					'errorGeneric'  => __( 'Error submitting donation. Please try again.', 'ambassadorhub' ),
				),
			)
		);
	}

	/**
	 * Render the block.
	 *
	 * @param array $attributes Block attributes.
	 *
	 * @return string Block HTML.
	 */
	public static function render_block( array $attributes = array() ): string {
		global $post;

		$fundraiser_id = isset( $attributes['fundraiserId'] ) ? (int) $attributes['fundraiserId'] : 0;

		$title      = trim( (string) ( $attributes['title'] ?? '' ) );
		$title_html = '' !== $title ? '<h2 class="ambh-block-title">' . esc_html( $title ) . '</h2>' : '';

		if ( $fundraiser_id <= 0 ) {
			$queried_id = (int) get_queried_object_id();
			if ( $queried_id > 0 && FundraisersModule::POST_TYPE === get_post_type( $queried_id ) ) {
				$fundraiser_id = $queried_id;
			}
		}

		if ( $fundraiser_id <= 0 && isset( $post->ID ) && FundraisersModule::POST_TYPE === get_post_type( $post ) ) {
			$fundraiser_id = (int) $post->ID;
		}

		// If no fundraiser context, show a placeholder in the editor and a clear
		// notice to editors on the frontend instead of failing silently.
		if ( 0 === $fundraiser_id ) {
			if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
				return $title_html . '<div style="padding: 20px; background: #f0f0f1; border: 1px solid #ddd; text-align: center;">'
					. __( 'Fundraiser donation form. It renders on fundraiser pages or when a fundraiserId is set.', 'ambassadorhub' )
					. '</div>';
			}

			if ( current_user_can( 'edit_posts' ) ) {
				return '<p class="ambh-notice ambh-notice--warning">'
					. esc_html__( 'Donation Form block: no fundraiser context found. Place this block on a fundraiser page or set a fundraiser ID in the block attributes.', 'ambassadorhub' )
					. '</p>';
			}

			return '';
		}

		// Block donations for ended or closed fundraisers.
		if ( ! FundraisersHelpers::fundraiser_is_open( $fundraiser_id ) ) {
			return '<p class="ambh-notice">' . esc_html( \AmbassadorHub\Fundraisers\Admin\Settings\SettingsManager::get_closed_message() ) . '</p>';
		}

		// Block donation form when the root fundraiser is configured as a category.
		if ( ! FundraisersHelpers::fundraiser_accepts_donations( $fundraiser_id ) ) {
			return '';
		}

		// Get predefined amounts and allow anonymous from global settings.
		$predefined_amounts = \AmbassadorHub\Fundraisers\Admin\Settings\SettingsManager::get_predefined_amounts_array();
		$allow_anonymous    = \AmbassadorHub\Fundraisers\Admin\Settings\SettingsManager::allow_anonymous();
		$collect_address    = \AmbassadorHub\Fundraisers\Admin\Settings\SettingsManager::collect_address();

		// Get minimum donation amount from settings.
		$min_donation = floatval( get_option( 'ambh_fundraisers_min_donation' ) );
		$max_donation = floatval( get_option( 'ambh_fundraisers_max_donation' ) );

		// Remove predefined amounts below the minimum so invalid options are never shown.
		if ( $min_donation > 0 ) {
			$predefined_amounts = array_values(
				array_filter( $predefined_amounts, fn( $a ) => $a >= $min_donation )
			);
		}

		$action_donate = esc_url( admin_url( 'admin-post.php?action=ambh_submit_donation' ) );
		$is_test_psp   = 'free' === (string) get_option( 'ambh_active_connector_psp', '' );
		$team_members  = self::get_team_members( $fundraiser_id );

		// Check for success/error messages.
		$message      = '';
		$message_type = '';
		if ( isset( $_GET['donation_success'] ) && '1' === $_GET['donation_success'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
			$message      = __( 'Thank you for your donation! Payment processing will follow soon.', 'ambassadorhub' );
			$message_type = 'success';
		} elseif ( isset( $_GET['donation_error'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
			$message      = sanitize_text_field( wp_unslash( $_GET['donation_error'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
			$message_type = 'error';
		}

		// Restore previously submitted form data after a validation error.
		//
		// The URL only ever carries an opaque bearer token (see
		// redirect_with_error()); it's format-checked here, but the value that
		// actually reaches get_transient()/delete_transient() is always
		// donation_form_transient_key()'s wp_hash() of it, never the raw
		// request value itself.
		$old = array();
		if ( isset( $_GET['donation_form_key'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
			$token = sanitize_text_field( wp_unslash( $_GET['donation_form_key'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
			if ( preg_match( '/^[A-Za-z0-9]{12}$/', $token ) ) {
				$transient_key  = self::donation_form_transient_key( $token );
				$transient_data = get_transient( $transient_key );
				if ( is_array( $transient_data ) ) {
					$old = $transient_data;
					delete_transient( $transient_key );
				}
			}
		}

		ob_start();
		echo $title_html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped above via esc_html().
		?>
		<div class="donation-form-block" data-fundraiser-id="<?php echo esc_attr( $fundraiser_id ); ?>">
			
			<?php if ( $message ) : ?>
				<div class="form-message form-message-<?php echo esc_attr( $message_type ); ?>">
					<?php echo esc_html( $message ); ?>
				</div>
			<?php endif; ?>

			<?php if ( $is_test_psp ) : ?>
				<div class="ambh-donation-form-notice ambh-donation-form-notice--test">
					<?php esc_html_e( 'Test mode is active. This donation will be processed as a test donation and completed instantly.', 'ambassadorhub' ); ?>
				</div>
			<?php endif; ?>
			
			<form class="donation-form" method="post" action="<?php echo esc_url( $action_donate ); ?>" data-min-amount="<?php echo esc_attr( $min_donation ); ?>" data-max-amount="<?php echo esc_attr( $max_donation ); ?>" data-allow-anonymous="<?php echo esc_attr( $allow_anonymous ? '1' : '0' ); ?>">
				<?php wp_nonce_field( 'ambh_donation_form', '_ambh_nonce' ); ?>
				<?php SpamShield::render_protection_fields( 'donation' ); ?>
				<input type="hidden" name="fundraiser_id" value="<?php echo esc_attr( $fundraiser_id ); ?>">
				<input type="hidden" name="redirect_url" value="<?php echo esc_url( get_permalink( $fundraiser_id ) ); ?>">

				<div class="ambh-step-indicator" aria-hidden="true">
					<span class="ambh-step-pip ambh-step-pip--active"></span>
					<span class="ambh-step-pip"></span>
				</div>

				<div class="ambh-form-step ambh-form-step--active" data-step="1">

				<div class="donor-info-fields">
					<p>
					<label for="donor_first_name"><?php echo esc_html__( 'First name', 'ambassadorhub' ); ?> <span class="required-indicator">*</span></label>
					<input type="text" id="donor_first_name" name="donor_first_name" autocomplete="given-name" value="<?php echo esc_attr( $old['donor_first_name'] ?? '' ); ?>" required>
				</p>

				<p>
					<label for="donor_last_name"><?php echo esc_html__( 'Last name', 'ambassadorhub' ); ?> <span class="required-indicator">*</span></label>
					<input type="text" id="donor_last_name" name="donor_last_name" autocomplete="family-name" value="<?php echo esc_attr( $old['donor_last_name'] ?? '' ); ?>" required>
				</p>

				<p>
					<label for="donor_email"><?php echo esc_html__( 'Email address', 'ambassadorhub' ); ?> <span class="required-indicator">*</span></label>
					<input type="email" id="donor_email" name="donor_email" autocomplete="email" value="<?php echo esc_attr( $old['donor_email'] ?? '' ); ?>" required>
				</p>
				</div>

				<?php if ( ! empty( $team_members ) ) : ?>
				<p class="ambh-donate-to">
					<label for="donate_to_fundraiser_id"><?php esc_html_e( 'Donate to', 'ambassadorhub' ); ?></label>
					<select id="donate_to_fundraiser_id" name="donate_to_fundraiser_id" class="ambh-form-select">
						<option value="">
						<?php
						/* translators: %s: team fundraiser title. */
						echo esc_html( sprintf( __( 'Team: %s', 'ambassadorhub' ), get_the_title( $fundraiser_id ) ) );
						?>
						</option>
						<option disabled aria-hidden="true">───────────────</option>
						<?php foreach ( $team_members as $ambh_member ) : ?>
							<option value="<?php echo esc_attr( $ambh_member->ID ); ?>"<?php selected( $ambh_member->ID, $old['donate_to_fundraiser_id'] ?? 0 ); ?>><?php echo esc_html( $ambh_member->post_title ); ?></option>
						<?php endforeach; ?>
					</select>
				</p>
				<?php endif; ?>

				<?php if ( ! empty( $predefined_amounts ) ) : ?>
					<?php
					$ambh_amounts_list  = array_values( $predefined_amounts );
					$ambh_preselect_idx = count( $ambh_amounts_list ) > 1 ? 1 : 0;
					$ambh_preselect_val = $ambh_amounts_list[ $ambh_preselect_idx ] ?? '';

					// When restoring after an error, determine which amount state to show.
					$old_selected_amount = isset( $old['selected_amount'] ) ? (string) $old['selected_amount'] : '';
					$old_amount_raw      = $old['amount'] ?? '';
					$old_is_custom       = ! empty( $old ) && ( '' === $old_selected_amount || ! in_array( (float) $old_selected_amount, array_map( 'floatval', $ambh_amounts_list ), true ) );
					?>
				<div class="donation-amount-section">
					<label><?php echo esc_html( sprintf( /* translators: %s: currency symbol */ __( 'Choose amount (%s)', 'ambassadorhub' ), SettingsManager::get_currency_symbol() ) ); ?> *</label>
					<div class="predefined-amounts">
						<?php foreach ( $ambh_amounts_list as $ambh_idx => $amount ) : ?>
							<?php
							if ( ! empty( $old ) ) {
								$ambh_btn_active = ! $old_is_custom && (float) $old_selected_amount === (float) $amount;
							} else {
								$ambh_btn_active = $ambh_idx === $ambh_preselect_idx;
							}
							?>
							<button type="button" class="amount-btn<?php echo $ambh_btn_active ? ' active' : ''; ?>" data-amount="<?php echo esc_attr( $amount ); ?>">
								<?php echo esc_html( SettingsManager::format_currency( $amount, 0 ) ); ?>
							</button>
						<?php endforeach; ?>
						<button type="button" class="amount-btn amount-btn-custom<?php echo ( ! empty( $old ) && $old_is_custom ) ? ' active' : ''; ?>" data-amount="custom">
							<?php echo esc_html__( 'Other amount', 'ambassadorhub' ); ?>
						</button>
					</div>
					<div class="custom-amount-field" style="<?php echo ( ! empty( $old ) && $old_is_custom ) ? '' : 'display: none;'; ?>">
						<label for="amount"><?php echo esc_html( sprintf( /* translators: %s: currency symbol */ __( 'Your amount (%s)', 'ambassadorhub' ), SettingsManager::get_currency_symbol() ) ); ?> *</label>
						<input type="text" inputmode="decimal" id="amount" name="amount" class="ambh-currency-input" value="<?php echo esc_attr( $old_amount_raw ); ?>" required>
							<small class="description"><?php /* translators: %s: formatted minimum donation amount */ echo esc_html( sprintf( __( 'Minimum: %s', 'ambassadorhub' ), SettingsManager::format_currency( $min_donation ) ) ); ?></small>
					</div>
					<input type="hidden" id="selected-amount" name="selected_amount" value="<?php echo esc_attr( ! empty( $old ) ? ( $old_is_custom ? '' : $old_selected_amount ) : $ambh_preselect_val ); ?>" required>
				</div>
				<?php else : ?>
				<div class="form-group">
					<label for="amount"><?php echo esc_html( sprintf( /* translators: %s: currency symbol */ __( 'Amount (%s)', 'ambassadorhub' ), SettingsManager::get_currency_symbol() ) ); ?> *</label>
					<input type="text" inputmode="decimal" id="amount" name="amount" class="ambh-currency-input" value="<?php echo esc_attr( $old['amount'] ?? '' ); ?>" required>
					<small class="description"><?php /* translators: %s: formatted minimum donation amount */ echo esc_html( sprintf( __( 'Minimum: %s', 'ambassadorhub' ), SettingsManager::format_currency( $min_donation ) ) ); ?></small>
				</div>
				<?php endif; ?>

				<button type="button" class="ambh-btn-next wp-block-button__link wp-element-button">
					<?php esc_html_e( 'Continue', 'ambassadorhub' ); ?> <span aria-hidden="true">→</span>
				</button>

				</div><!-- /.ambh-form-step[1] -->

				<div class="ambh-form-step" data-step="2">

				<?php if ( $collect_address ) : ?>
				<div class="donor-address-fields">
					<p class="ambh-form-section-label"><?php esc_html_e( 'Address', 'ambassadorhub' ); ?></p>

					<p>
						<label for="donor_street"><?php esc_html_e( 'Street address', 'ambassadorhub' ); ?></label>
						<input type="text" id="donor_street" name="donor_street" autocomplete="street-address" value="<?php echo esc_attr( $old['donor_street'] ?? '' ); ?>">
					</p>

					<div class="ambh-address-inline">
						<p>
							<label for="donor_postal_code"><?php esc_html_e( 'Postal code', 'ambassadorhub' ); ?></label>
							<input type="text" id="donor_postal_code" name="donor_postal_code" autocomplete="postal-code" value="<?php echo esc_attr( $old['donor_postal_code'] ?? '' ); ?>">
						</p>
						<p>
							<label for="donor_city"><?php esc_html_e( 'City', 'ambassadorhub' ); ?></label>
							<input type="text" id="donor_city" name="donor_city" autocomplete="address-level2" value="<?php echo esc_attr( $old['donor_city'] ?? '' ); ?>">
						</p>
					</div>

					<p>
						<label for="donor_country"><?php esc_html_e( 'Country', 'ambassadorhub' ); ?></label>
						<select id="donor_country" name="donor_country" autocomplete="country" class="ambh-country-select">
							<option value=""><?php esc_html_e( '— Select country —', 'ambassadorhub' ); ?></option>
							<?php foreach ( self::get_countries() as $code => $label ) : ?>
								<option value="<?php echo esc_attr( $code ); ?>"<?php selected( $code, $old['donor_country'] ?? '' ); ?>><?php echo esc_html( $label ); ?></option>
							<?php endforeach; ?>
						</select>
					</p>
				</div>
				<?php endif; ?>

				<p>
					<label for="message"><?php echo esc_html__( 'Message (optional)', 'ambassadorhub' ); ?></label>
					<textarea id="message" name="message" rows="3" placeholder="<?php echo esc_attr__( 'Leave a message that will be visible to everyone', 'ambassadorhub' ); ?>"><?php echo esc_textarea( $old['message'] ?? '' ); ?></textarea>
				</p>

				<?php
				// Render add-on contributed donation-form fields (custom fields, consent
				// checkboxes, etc.) scoped to the current donation context. The container
				// mirrors the create-fundraiser-form pattern so a fragment endpoint can be
				// wired in the future if/when context dimensions become dynamic.
				$ambh_donation_form_context = array(
					'fundraiser_id' => (int) $fundraiser_id,
					'donation_id'   => 0,
				);
				?>
				<div class="ambh-donation-form-fields" data-ambh-form-fields-container>
					<?php do_action( 'ambh_render_donation_form_fields', $ambh_donation_form_context ); ?>
				</div>

				<?php if ( $allow_anonymous ) : ?>
				<div class="ambh-consent-checkbox">
					<label>
						<input type="checkbox" name="is_anonymous" value="1" id="is_anonymous"<?php checked( $old['is_anonymous'] ?? '', '1' ); ?>>
						<?php echo esc_html__( 'I want to donate anonymously', 'ambassadorhub' ); ?>
						<span class="anon-info-icon" aria-label="<?php echo esc_attr__( 'What does anonymous mean?', 'ambassadorhub' ); ?>" tabindex="0">
							&#9432;
							<span class="anon-info-tooltip" role="tooltip">
								<?php echo esc_html__( 'Your name will appear as "Anonymous" on the website and will not be shared with the organiser. Your contact details are only used for administrative purposes.', 'ambassadorhub' ); ?>
							</span>
						</span>
					</label>
				</div>
				<?php endif; ?>
				
				<div class="ambh-form-nav">
					<button type="button" class="ambh-btn-back">
						<span aria-hidden="true">←</span> <?php esc_html_e( 'Back', 'ambassadorhub' ); ?>
					</button>
					<button type="submit" class="wp-block-button__link wp-element-button donation-submit-btn"><?php esc_html_e( 'Donate now', 'ambassadorhub' ); ?></button>
				</div>

				</div><!-- /.ambh-form-step[2] -->
			</form>
		</div>
		<?php
		return ob_get_clean();
	}

	/**
	 * Get published individual (non-team) child fundraisers of a team.
	 *
	 * @param int $team_id Team fundraiser post ID.
	 * @return \WP_Post[]
	 */
	private static function get_team_members( int $team_id ): array {
		if ( ! FundraisersHelpers::fundraiser_is_team( $team_id ) ) {
			return array();
		}

		return get_posts(
			array(
				'post_type'      => FundraisersModule::POST_TYPE,
				'post_status'    => 'publish',
				'post_parent'    => $team_id,
				'posts_per_page' => -1,
				'orderby'        => 'title',
				'order'          => 'ASC',
				// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- already scoped to one team's direct children via post_parent above; there's no non-meta way to distinguish member fundraisers from sub-teams.
				'meta_query'     => array(
					'relation' => 'OR',
					array(
						'key'     => FundraisersModule::META_IS_TEAM,
						'compare' => 'NOT EXISTS',
					),
					array(
						'key'     => FundraisersModule::META_IS_TEAM,
						'value'   => '1',
						'compare' => '!=',
					),
				),
			)
		);
	}

	/**
	 * Return an ordered list of ISO 3166-1 alpha-2 country codes → names.
	 *
	 * The Netherlands is listed first as the primary market; the rest are
	 * sorted alphabetically by English name.
	 *
	 * @return array<string,string>
	 */
	private static function get_countries(): array {
		return array(
			'NL' => __( 'Netherlands', 'ambassadorhub' ),
			'AT' => __( 'Austria', 'ambassadorhub' ),
			'AU' => __( 'Australia', 'ambassadorhub' ),
			'BE' => __( 'Belgium', 'ambassadorhub' ),
			'CA' => __( 'Canada', 'ambassadorhub' ),
			'CH' => __( 'Switzerland', 'ambassadorhub' ),
			'CN' => __( 'China', 'ambassadorhub' ),
			'CZ' => __( 'Czech Republic', 'ambassadorhub' ),
			'DE' => __( 'Germany', 'ambassadorhub' ),
			'DK' => __( 'Denmark', 'ambassadorhub' ),
			'ES' => __( 'Spain', 'ambassadorhub' ),
			'FI' => __( 'Finland', 'ambassadorhub' ),
			'FR' => __( 'France', 'ambassadorhub' ),
			'GB' => __( 'United Kingdom', 'ambassadorhub' ),
			'GR' => __( 'Greece', 'ambassadorhub' ),
			'HU' => __( 'Hungary', 'ambassadorhub' ),
			'IE' => __( 'Ireland', 'ambassadorhub' ),
			'IN' => __( 'India', 'ambassadorhub' ),
			'IT' => __( 'Italy', 'ambassadorhub' ),
			'JP' => __( 'Japan', 'ambassadorhub' ),
			'LU' => __( 'Luxembourg', 'ambassadorhub' ),
			'MX' => __( 'Mexico', 'ambassadorhub' ),
			'NO' => __( 'Norway', 'ambassadorhub' ),
			'NZ' => __( 'New Zealand', 'ambassadorhub' ),
			'PL' => __( 'Poland', 'ambassadorhub' ),
			'PT' => __( 'Portugal', 'ambassadorhub' ),
			'RO' => __( 'Romania', 'ambassadorhub' ),
			'SE' => __( 'Sweden', 'ambassadorhub' ),
			'SG' => __( 'Singapore', 'ambassadorhub' ),
			'SK' => __( 'Slovakia', 'ambassadorhub' ),
			'TR' => __( 'Turkey', 'ambassadorhub' ),
			'UA' => __( 'Ukraine', 'ambassadorhub' ),
			'US' => __( 'United States', 'ambassadorhub' ),
			'ZA' => __( 'South Africa', 'ambassadorhub' ),
		);
	}

	/**
	 * Render shortcode.
	 *
	 * @param array $atts Shortcode attributes.
	 *
	 * @return string
	 */
	public static function render_shortcode( $atts ): string {
		$atts = shortcode_atts( array( 'id' => get_the_ID() ), $atts );
		return self::render_block( array( 'fundraiserId' => (int) $atts['id'] ) );
	}

	/**
	 * Redirect back to the form with an error message and preserved form data.
	 *
	 * Stores the submitted field values in a short-lived transient so the form
	 * can restore them after the redirect, preventing data loss on validation errors.
	 *
	 * @param string $redirect_url Destination URL (the fundraiser page).
	 * @param string $message      Translated error string.
	 * @param array  $form_data    Sanitized field values to restore.
	 * @return void
	 */
	private static function redirect_with_error( string $redirect_url, string $message, array $form_data ): void {
		$token = wp_generate_password( 12, false );
		set_transient( self::donation_form_transient_key( $token ), $form_data, 5 * MINUTE_IN_SECONDS );
		wp_safe_redirect(
			add_query_arg(
				array(
					'donation_error'    => rawurlencode( $message ),
					'donation_form_key' => $token,
				),
				$redirect_url
			)
		);
		exit;
	}

	/**
	 * Derive the transient name used to stash/restore form data across the
	 * validation-error redirect.
	 *
	 * The client only ever sees/sends the raw bearer $token; the transient
	 * actually stored in the options table is named after a wp_hash() of it,
	 * so a request-supplied value is never itself used as a transient name —
	 * only as input to a one-way hash of it.
	 *
	 * @param string $token Raw token (see redirect_with_error()).
	 * @return string
	 */
	private static function donation_form_transient_key( string $token ): string {
		return 'ambh_dfd_' . wp_hash( $token );
	}

	/**
	 * Handle donation form submission.
	 *
	 * @return void
	 */
	public static function handle_donation_submission(): void {
		// Verify nonce.
		if ( ! isset( $_POST['_ambh_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_ambh_nonce'] ) ), 'ambh_donation_form' ) ) {
			wp_die( esc_html__( 'Security check failed.', 'ambassadorhub' ) );
		}

		// Get redirect URL.
		$redirect_url = isset( $_POST['redirect_url'] ) ? esc_url_raw( wp_unslash( $_POST['redirect_url'] ) ) : home_url();

		// Rate limit + honeypot/timing check — reject likely-automated submissions
		// before creating any donation record. See AmbassadorHub\Spam\SpamShield.
		if ( ! SpamShield::throttle_by_ip( 'donation', SpamShield::donation_rate_limit(), 10 * MINUTE_IN_SECONDS ) ) {
			self::redirect_with_error( $redirect_url, __( 'Too many submissions. Please wait a while and try again.', 'ambassadorhub' ), array() );
		}
		if ( ! SpamShield::passed( 'donation' ) ) {
			self::redirect_with_error( $redirect_url, __( 'Your submission looked automated and was blocked. Please try again.', 'ambassadorhub' ), array() );
		}

		// Validate required fields.
		$fundraiser_id = isset( $_POST['fundraiser_id'] ) ? intval( wp_unslash( $_POST['fundraiser_id'] ) ) : 0;
		$is_anonymous  = isset( $_POST['is_anonymous'] ) && sanitize_text_field( wp_unslash( $_POST['is_anonymous'] ) ) === '1';

		// Always collect donor contact details — is_anonymous only controls frontend display.
		$donor_first_name  = isset( $_POST['donor_first_name'] ) ? sanitize_text_field( wp_unslash( $_POST['donor_first_name'] ) ) : '';
		$donor_last_name   = isset( $_POST['donor_last_name'] ) ? sanitize_text_field( wp_unslash( $_POST['donor_last_name'] ) ) : '';
		$donor_email       = isset( $_POST['donor_email'] ) ? sanitize_email( wp_unslash( $_POST['donor_email'] ) ) : '';
		$donor_street      = isset( $_POST['donor_street'] ) ? sanitize_text_field( wp_unslash( $_POST['donor_street'] ) ) : '';
		$donor_city        = isset( $_POST['donor_city'] ) ? sanitize_text_field( wp_unslash( $_POST['donor_city'] ) ) : '';
		$donor_postal_code = isset( $_POST['donor_postal_code'] ) ? sanitize_text_field( wp_unslash( $_POST['donor_postal_code'] ) ) : '';
		$donor_country     = isset( $_POST['donor_country'] ) ? sanitize_text_field( wp_unslash( $_POST['donor_country'] ) ) : '';

		// When address collection is disabled, never persist address data even if it
		// was somehow submitted (the fields aren't rendered, but don't trust the client).
		if ( ! SettingsManager::collect_address() ) {
			$donor_street      = '';
			$donor_city        = '';
			$donor_postal_code = '';
			$donor_country     = '';
		}

		// Get amount from selected_amount if predefined amounts were used, otherwise from amount field.
		// normalize_amount() handles locale-specific decimal separators (e.g. comma for EUR).
		$amount = isset( $_POST['selected_amount'] ) && ! empty( $_POST['selected_amount'] )
			? SettingsManager::normalize_amount( sanitize_text_field( wp_unslash( $_POST['selected_amount'] ) ) )
			: ( isset( $_POST['amount'] ) ? SettingsManager::normalize_amount( sanitize_text_field( wp_unslash( $_POST['amount'] ) ) ) : 0.0 );

		$message = isset( $_POST['message'] ) ? sanitize_textarea_field( wp_unslash( $_POST['message'] ) ) : '';

		// Collect raw input values so they can be restored if validation fails.
		$selected_amount_raw = isset( $_POST['selected_amount'] ) ? sanitize_text_field( wp_unslash( $_POST['selected_amount'] ) ) : '';
		$amount_raw          = isset( $_POST['amount'] ) ? sanitize_text_field( wp_unslash( $_POST['amount'] ) ) : '';
		$donate_to_raw       = isset( $_POST['donate_to_fundraiser_id'] ) ? absint( wp_unslash( $_POST['donate_to_fundraiser_id'] ) ) : 0;

		$form_data = array(
			'donor_first_name'        => $donor_first_name,
			'donor_last_name'         => $donor_last_name,
			'donor_email'             => $donor_email,
			'donor_street'            => $donor_street,
			'donor_city'              => $donor_city,
			'donor_postal_code'       => $donor_postal_code,
			'donor_country'           => $donor_country,
			'message'                 => $message,
			'is_anonymous'            => $is_anonymous ? '1' : '',
			'selected_amount'         => $selected_amount_raw,
			'amount'                  => $amount_raw,
			'donate_to_fundraiser_id' => $donate_to_raw,
		);

		// Validate required fields (name and email only required if not anonymous).
		if ( ! $fundraiser_id || $amount <= 0 ) {
			self::redirect_with_error( $redirect_url, __( 'Please fill in all required fields.', 'ambassadorhub' ), $form_data );
		}

		if ( $donor_email && SpamShield::is_blocked_email( $donor_email ) ) {
			SpamShield::log( 'donation', 'blocked_email' );
			self::redirect_with_error( $redirect_url, __( 'This email address cannot be used for donations. Please use a different address.', 'ambassadorhub' ), $form_data );
		}

		if ( SpamShield::has_too_many_links( $message ) ) {
			SpamShield::log( 'donation', 'too_many_links' );
			self::redirect_with_error( $redirect_url, __( 'Please remove links from your message and try again.', 'ambassadorhub' ), $form_data );
		}

		// Gate: reject donation if fundraiser is closed or ended.
		if ( ! FundraisersHelpers::fundraiser_is_open( $fundraiser_id ) ) {
			self::redirect_with_error( $redirect_url, \AmbassadorHub\Fundraisers\Admin\Settings\SettingsManager::get_closed_message(), $form_data );
		}

		// Gate: reject donation if the root fundraiser does not accept donations.
		if ( ! FundraisersHelpers::fundraiser_accepts_donations( $fundraiser_id ) ) {
			self::redirect_with_error( $redirect_url, __( 'This fundraiser does not accept direct donations.', 'ambassadorhub' ), $form_data );
		}

		if ( empty( $donor_first_name ) || empty( $donor_last_name ) || empty( $donor_email ) ) {
			self::redirect_with_error( $redirect_url, __( 'Please fill in all required fields.', 'ambassadorhub' ), $form_data );
		}

		if ( ! SpamShield::throttle_by_email( 'donation', $donor_email, 5, HOUR_IN_SECONDS ) ) {
			self::redirect_with_error( $redirect_url, __( 'Too many submissions for this email address. Please try again later.', 'ambassadorhub' ), $form_data );
		}

		// Validate minimum and maximum donation amount.
		$min_donation = floatval( get_option( 'ambh_fundraisers_min_donation' ) );
		$max_donation = floatval( get_option( 'ambh_fundraisers_max_donation' ) );

		if ( $amount < $min_donation ) {
			/* translators: %s: formatted minimum donation amount */
			self::redirect_with_error( $redirect_url, sprintf( __( 'Minimum donation amount is %s.', 'ambassadorhub' ), SettingsManager::format_currency( $min_donation ) ), $form_data );
		}
		if ( $max_donation > 0 && $amount > $max_donation ) {
			/* translators: %s: formatted maximum donation amount */
			self::redirect_with_error( $redirect_url, sprintf( __( 'Maximum donation amount is %s.', 'ambassadorhub' ), SettingsManager::format_currency( $max_donation ) ), $form_data );
		}

		// Verify fundraiser exists.
		$fundraiser = get_post( $fundraiser_id );
		if ( ! $fundraiser || FundraisersModule::POST_TYPE !== $fundraiser->post_type ) {
			self::redirect_with_error( $redirect_url, __( 'Invalid fundraiser.', 'ambassadorhub' ), $form_data );
		}

		// If a team member was selected, redirect the donation to that member instead.
		$donate_to_id = isset( $_POST['donate_to_fundraiser_id'] ) ? absint( wp_unslash( $_POST['donate_to_fundraiser_id'] ) ) : 0;
		if ( $donate_to_id > 0 && FundraisersHelpers::fundraiser_is_team( $fundraiser_id ) ) {
			$member_post = get_post( $donate_to_id );
			if (
				$member_post &&
				FundraisersModule::POST_TYPE === $member_post->post_type &&
				'publish' === $member_post->post_status &&
				(int) $member_post->post_parent === $fundraiser_id &&
				! FundraisersHelpers::fundraiser_is_team( $donate_to_id )
			) {
				$fundraiser_id = $donate_to_id;
				$fundraiser    = $member_post;
			}
		}

		// Require an active PSP connector before accepting a donation.
		if ( ! ConnectorRegistry::get_active_psp() ) {
			self::redirect_with_error( $redirect_url, __( 'No payment provider has been configured. Please contact the site administrator.', 'ambassadorhub' ), $form_data );
		}

		// Create donation record (pending payment).
		$donation_id = DonationsModule::create(
			array(
				'fundraiser_id'     => $fundraiser_id,
				'donor_first_name'  => $donor_first_name,
				'donor_last_name'   => $donor_last_name,
				'donor_email'       => $donor_email,
				'donor_street'      => $donor_street,
				'donor_city'        => $donor_city,
				'donor_postal_code' => $donor_postal_code,
				'donor_country'     => $donor_country,
				'is_anonymous'      => $is_anonymous ? 1 : 0,
				'amount'            => $amount,
				'currency'          => 'EUR',
				'status'            => 'pending',
				'message'           => $message,
				'notes'             => __( 'Created via donation form. Waiting for payment processing.', 'ambassadorhub' ),
			)
		);

		if ( ! $donation_id ) {
			self::redirect_with_error( $redirect_url, __( 'Error saving donation.', 'ambassadorhub' ), $form_data );
		}

		/**
		 * Fires after a donation is created via the public donation form.
		 *
		 * Extensions hook here to persist additional submitted data (e.g.
		 * custom fields) tied to the new donation, before the PSP redirect.
		 * Saving here — rather than only after PSP success — ensures custom
		 * field data is captured even if payment initiation fails.
		 *
		 * @param int   $donation_id The new donation ID.
		 * @param array $post_data   The raw $_POST submission payload.
		 */
		do_action(
			'ambh_donation_form_saved',
			$donation_id,
			RequestData::sanitized_copy( $_POST, array( '_ambh_nonce' ) ) // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified at the top of the handler.
		);

		// Build the post-payment return URL from the configured page, falling back to home.
		$psp_redirect_page_id = (int) get_option( 'ambh_psp_redirect_page_id', 0 );
		$psp_redirect_url     = add_query_arg(
			'donation_id',
			$donation_id,
			$psp_redirect_page_id > 0 ? get_permalink( $psp_redirect_page_id ) : home_url( '/' )
		);

		// Hand off to the active PSP connector.
		$psp    = ConnectorRegistry::get_active_psp();
		$result = $psp::initiate_payment(
			array(
				'donation_id'  => $donation_id,
				'amount'       => $amount,
				'currency'     => 'EUR',
				'description'  => sprintf(
					/* translators: %s: fundraiser title */
					__( 'Donation to %s', 'ambassadorhub' ),
					get_the_title( $fundraiser_id )
				),
				'redirect_url' => $psp_redirect_url,
				'donor'        => array(
					'first_name' => $donor_first_name,
					'last_name'  => $donor_last_name,
					'email'      => $donor_email,
				),
			)
		);

		if ( $result->success && $result->redirect_url ) {

			do_action(
				'ambh_successful_donation_form_submission',
				$donation_id,
				RequestData::sanitized_copy( $_POST, array( '_ambh_nonce' ) )
			); // Hook for handling additional form fields, e.g. consent checkbox.

			// Persist the PSP transaction ID so the webhook handler can look it up later.
			if ( $result->transaction_id ) {
				DonationsModule::update( $donation_id, array( 'transaction_id' => $result->transaction_id ) );
			}
			// Redirect donor to the PSP-hosted checkout page (may be an external URL).
			wp_redirect( $result->redirect_url ); // phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect
			exit;
		}

		// PSP initiation failed — leave the donation row as pending for admin review.
		self::redirect_with_error( $redirect_url, $result->error_message ?? __( 'Payment could not be started. Please try again.', 'ambassadorhub' ), $form_data );
	}
}
