<?php
/**
 * Card server logic.
 *
 * @package ThemeIsle\GutenbergBlocks\Server
 */

namespace ThemeIsle\GutenbergBlocks\Server;

use Exception;
use ThemeIsle\GutenbergBlocks\Integration\Form_Data_Request;
use ThemeIsle\GutenbergBlocks\Integration\Form_Data_Response;
use ThemeIsle\GutenbergBlocks\Integration\Form_Email;
use ThemeIsle\GutenbergBlocks\Integration\Form_Field_WP_Option_Data;
use ThemeIsle\GutenbergBlocks\Integration\Form_Providers;
use ThemeIsle\GutenbergBlocks\Integration\Form_Settings_Data;
use ThemeIsle\GutenbergBlocks\Integration\Form_Utils;
use ThemeIsle\GutenbergBlocks\Integration\Mailchimp_Integration;
use ThemeIsle\GutenbergBlocks\Integration\Sendinblue_Integration;
use ThemeIsle\GutenbergBlocks\Plugins\Form_Submissions;
use ThemeIsle\GutenbergBlocks\Plugins\Stripe_API;
use WP_Error;
use WP_HTTP_Response;
use WP_REST_Request;
use WP_REST_Response;
use WP_REST_Server;

/**
 * Class Form_Server
 *
 * @since 2.0.0
 */
class Form_Server {

	/**
	 * The main instance var.
	 *
	 * @var Form_Server|null
	 * @since 2.0.0
	 */
	public static $instance = null;

	/**
	 * Rest route namespace.
	 *
	 * @var string
	 * @since 2.0.0
	 */
	public $namespace = 'otter/';

	/**
	 * Rest route version.
	 *
	 * @var string
	 * @since 2.0.0
	 */
	public $version = 'v1';

	/**
	 * Anti Spam timeout
	 */
	const ANTI_SPAM_TIMEOUT = 5000; // 5 seconds

	/**
	 * Autoresponder Email Error Expiration Time
	 */
	const AUTO_RESPONDER_ERROR_EXPIRATION_TIME = WEEK_IN_SECONDS;

	/**
	 * Admin Alert cooldown for infrastructure failures (captcha provider, delivery).
	 *
	 * At most one alert per form per failure type within this window — the Records and
	 * their Delivery Status remain the per-submission source of truth.
	 */
	const ADMIN_ALERT_EXPIRATION_TIME = HOUR_IN_SECONDS;

	/**
	 * Initialize the class
	 *
	 * @since 2.0.0
	 */
	public function init() {
		/**
		 * Register the REST API endpoints.
		 */
		add_action( 'rest_api_init', array( $this, 'register_routes' ) );

		/**
		 * Register email providers that can be used to send emails or subscribe to a service.
		 */
		$default_provider = array(
			'frontend' => array(
				'submit' => array( $this, 'send_default_email' ),
			),
			'editor'   => array(
				'listId'    => array( $this, 'get_integration_data' ),
				'testEmail' => array( $this, 'send_test_email' ),
			),
		);

		// Register 3rd party email providers.
		$sendinblue_provider = array(
			'frontend' => array(
				'submit' => array( $this, 'subscribe_to_service' ),
			),
			'editor'   => array(
				'listId'    => array( $this, 'get_integration_data' ),
				'testEmail' => array( $this, 'test_subscription_service' ),
			),
		);
		$mailchimp_provider  = array(
			'frontend' => array(
				'submit' => array( $this, 'subscribe_to_service' ),
			),
			'editor'   => array(
				'listId'    => array( $this, 'get_integration_data' ),
				'testEmail' => array( $this, 'test_subscription_service' ),
			),
		);

		do_action(
			'otter_register_form_providers',
			array(
				'default'    => $default_provider,
				'sendinblue' => $sendinblue_provider,
				'mailchimp'  => $mailchimp_provider,
			)
		);

		/**
		 * Register utility filters for form data validation.
		 */
		add_filter( 'otter_form_validate_form', array( $this, 'check_form_conditions' ) );
		add_filter( 'otter_form_validate_form', array( $this, 'check_form_files' ) );

		/**
		 * Register utility filters for bot detection (e.g.: captcha, honeypot methods).
		 */
		add_filter( 'otter_form_anti_spam_validation', array( $this, 'anti_spam_validation' ) );
		add_filter( 'otter_form_anti_spam_validation', array( $this, 'check_form_captcha' ) );

		/**
		 * Register utility filters for form data preparation (e.g.: uploading files, database queries).
		 */
		add_filter( 'otter_form_data_preparation', array( $this, 'change_provider_based_on_consent' ) );

		/**
		 * Register utility filters for email content building.
		 */
		add_filter( 'otter_form_email_build_body', array( $this, 'build_email_content' ) );
		add_filter( 'otter_form_email_build_body_error', array( $this, 'build_email_error_content' ), 1 );

		/**
		 * Register utility actions that triggers after the main submit action. Actions that clean the data, generated files or auxiliary actions.
		 */
		add_action( 'otter_form_after_submit', array( $this, 'after_submit' ) );

		/**
		 * Register utility actions for logging issues that occurred during the form submission.
		 */
		add_action( 'otter_form_issues_handler', array( $this, 'log_issues' ) );
		add_action( 'otter_form_issues_handler', array( $this, 'send_error_email_to_admin' ) );

		add_action( 'otter_form_on_submission_confirmed', array( $this, 'apply_main_provider' ) );
		add_filter( 'otter_form_session_confirmation', array( $this, 'verify_confirmation_session' ) );
	}

	/**
	 * Register REST API route
	 *
	 * @since 2.0.0
	 */
	public function register_routes() {
		$namespace = $this->namespace . $this->version;
		register_rest_route(
			$namespace,
			'/form/frontend',
			array(
				array(
					'methods'             => WP_REST_Server::CREATABLE,
					'callback'            => array( $this, 'frontend' ),
					'permission_callback' => function ( $request ) {
						$nonces = $request->get_header_as_array( 'X-WP-Nonce' );
						if ( isset( $nonces ) ) {
							foreach ( $nonces as $nonce ) {
								if ( wp_verify_nonce( $nonce, 'wp_rest' ) ) {
									return __return_true();
								}
							}
						}
						return __return_false();
					},
				),
			)
		);
		register_rest_route(
			$namespace,
			'/form/confirm',
			array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'confirm_form_submission' ),
					'permission_callback' => function ( $request ) {
						$session = $request->get_param( 'stripe_checkout' );

						if ( apply_filters( 'otter_form_session_confirmation', $session ) ) {
							return __return_true();
						}
						return false;
					},
					'args'                => array(
						'record_id' => array(
							'validate_callback' => function ( $param, $request, $key ) {
								return is_numeric( $param );
							},
						),
						'session'   => array(
							'validate_callback' => function ( $param, $request, $key ) {
								return is_string( $param );
							},
						),
					),
				),
			)
		);
		register_rest_route(
			$namespace,
			'/form/editor',
			array(
				array(
					'methods'             => WP_REST_Server::CREATABLE,
					'callback'            => array( $this, 'editor' ),
					'permission_callback' => function () {
						return current_user_can( 'edit_posts' );
					},
				),
			)
		);
	}

	/**
	 * Get information from the provider services.
	 *
	 * @param WP_REST_Request $request The API request.
	 * @return WP_Error|WP_HTTP_Response|WP_REST_Response
	 * @since 2.0.3
	 */
	public function editor( $request ) {
		$data = new Form_Data_Request( $request );
		$res  = new Form_Data_Response();

		$form_options = Form_Settings_Data::get_form_setting_from_wordpress_options( $data->get_data_from_payload( 'formOption' ) );
		$data->set_form_options( $form_options );

		// Select the handler functions based on the provider.
		$provider = $form_options->get_provider();
		if ( $data->payload_has( 'provider' ) ) {
			$provider = $data->get_data_from_payload( 'provider' );
		}
		$provider_handlers = Form_Providers::$instance->get_provider_handlers( $provider, 'editor' );

		if ( $data->has_error() ) {
			return $res->set_code( $data->get_error_code() )->build_response();
		}

		if ( $provider_handlers && Form_Providers::provider_has_handler( $provider_handlers, $data->get_root_data( 'handler' ) ) ) {
			// Send the data to the provider.
			return $provider_handlers[ $data->get_root_data( 'handler' ) ]( $data );
		} else {
			$res->set_error( __( 'The email service provider was not registered!', 'otter-blocks' ) );
		}

		return $res->build_response();
	}


	/**
	 * Handle the request from the form block
	 *
	 * @param WP_REST_Request $request Form request.
	 * @return WP_Error|WP_HTTP_Response|WP_REST_Response
	 * @throws \Exception Error.
	 * @since 2.0.3
	 */
	public function frontend( $request ) {
		$res       = new Form_Data_Response();
		$form_data = new Form_Data_Request( $request );

		// Validate the form data.
		$form_data = apply_filters( 'otter_form_validate_form', $form_data );

		if ( ! ( $form_data instanceof Form_Data_Request ) ) {
			$form_data = self::replace_corrupted_form_data( $request, $res );
		}

		$form_options = Form_Settings_Data::get_form_setting_from_wordpress_options( $form_data->get_data_from_payload( 'formOption' ) );
		$form_data->set_form_options( $form_options );
		$form_data = self::pull_fields_options_for_form( $form_data );

		/**
		 * The flow follows the railway programming pattern.
		 *
		 * If one the steps fails, the next steps will not be executed.
		 *
		 * For functions that are in the same hook, the order is important. E.g.: do not create a stripe session if some other validation fails.
		 */
		try {
			// Check bot validation.
			$form_data = apply_filters( 'otter_form_anti_spam_validation', $form_data );

			/**
			 * Apply additional actions before using the main handler function for submitting.
			 *
			 * @var Form_Data_Request $form_data
			 */
			$form_data = apply_filters( 'otter_form_data_preparation', $form_data );

			// Check if $form_data has function get_error_code. Otherwise, it will throw an error.
			if ( ! ( $form_data instanceof Form_Data_Request ) ) {
				$form_data = self::replace_corrupted_form_data( $request, $res );
				$form_data->set_form_options( $form_options );
			}

			if ( $res->has_error() || $form_data->has_error() ) {
				$res->set_code( $form_data->get_error_code() );
				do_action( 'otter_form_issues_handler', $form_data );
			} else {

				/**
				 * Persist the submission as a Record before attempting any delivery action,
				 * so a delivery failure can no longer lose the submission. The save handler
				 * stores the new Record ID on the form data.
				 *
				 * @param Form_Data_Request $form_data The form data.
				 * @since 3.1
				 */
				do_action( 'otter_form_record_save', $form_data );

				if ( $form_data->has_infrastructure_failure() ) {

					// A dependent service is down: the Record was saved and marked failed, but the submission cannot be verified/delivered.
					$res->set_code( $form_data->get_infrastructure_failure_code() )
						->set_display_error( $form_options->get_error_message() );

				} else {

					if ( ! empty( $form_options->get_redirect_link() ) ) {
						$res->add_response_field( 'redirectLink', $form_options->get_redirect_link() );
					}

					$this->apply_main_provider( $form_data ); // Send the data to the provider.

					do_action( 'otter_form_after_submit', $form_data );

					if ( ! ( $form_data instanceof Form_Data_Request ) ) {
						$form_data = self::replace_corrupted_form_data( $request, $res );
						$form_data->set_form_options( $form_options );
					}

					if ( $form_data->has_error() ) {
						$res->set_code( $form_data->get_error_code() )
							->set_display_error( $form_options->get_error_message() );
					} else {
						$res->set_code( Form_Data_Response::SUCCESS_EMAIL_SEND )
							->set_success_message( $form_options->get_submit_message() )
							->mark_as_success();
					}
				}
			}
		} catch ( Exception $e ) {
			$res->set_code( Form_Data_Response::ERROR_RUNTIME_ERROR )
				->add_reason( $e->getMessage() );
			$form_data->set_error( Form_Data_Response::ERROR_RUNTIME_ERROR );
			$form_data->add_warning( Form_Data_Response::ERROR_RUNTIME_ERROR, $e->getMessage() );
		} finally {
			if ( $form_data instanceof Form_Data_Request ) {
				$form_data->append_metadata( $res );
			}

			do_action( 'otter_form_issues_handler', $form_data );
			return $res->build_response();
		}
	}

	/**
	 * Replace a form data object that was corrupted by a third-party hook, so that a graceful runtime error response can be returned.
	 *
	 * @param WP_REST_Request    $request The original form request.
	 * @param Form_Data_Response $res The response on which to record the runtime error.
	 * @phpstan-param \WP_REST_Request<array<string, mixed>> $request
	 * @return Form_Data_Request A fresh form data object carrying the runtime error, safe for downstream calls.
	 * @since 3.1
	 */
	private static function replace_corrupted_form_data( $request, $res ) {
		$res->set_code( Form_Data_Response::ERROR_RUNTIME_ERROR )
			->add_reason( __( 'The form data class is not valid after performing provider actions! Some hook is corrupting the data.', 'otter-blocks' ) );

		$form_data = new Form_Data_Request( $request );
		$form_data->set_error( Form_Data_Response::ERROR_RUNTIME_ERROR, __( 'The form data class is not valid after performing provider actions! Some hook is corrupting the data.', 'otter-blocks' ) );

		return $form_data;
	}

	/**
	 * Confirm form submission.
	 *
	 * @param WP_REST_Request $request Form request.
	 * @return WP_Error|WP_HTTP_Response|WP_REST_Response
	 */
	public function confirm_form_submission( $request ) {

		$response = new Form_Data_Response();

		try {
			$response = apply_filters( 'otter_form_record_confirm', $response, $request );
		} catch ( Exception $e ) {
			$response->set_code( Form_Data_Response::ERROR_RUNTIME_STRIPE_SESSION_VALIDATION );
			$response->add_reason( $e->getMessage() );
		} finally {
			return $response->build_response();
		}
	}

	/**
	 * Apply the main provider.
	 *
	 * @param Form_Data_Request|null $form_data Form data.
	 * @return void
	 */
	public function apply_main_provider( $form_data ) {

		if ( ! isset( $form_data ) ) {
			return;
		}

		if ( $form_data->has_error() || $form_data->is_temporary() ) {
			return;
		}

		$provider_handlers = apply_filters( 'otter_select_form_provider', $form_data );

		if ( $provider_handlers && Form_Providers::provider_has_handler( $provider_handlers, $form_data->get_root_data( 'handler' ) ) ) {

			// Send the data to the provider handler.
			$provider_handlers[ $form_data->get_root_data( 'handler' ) ]( $form_data );
		} else {
			$form_data->set_error( Form_Data_Response::ERROR_PROVIDER_NOT_REGISTERED );
		}
	}

	/**
	 * Send Email using SMTP.
	 *
	 * @param Form_Data_Request|null $form_data Data from request body.
	 * @return Form_Data_Request|null
	 * @since 2.0.3
	 */
	public function send_default_email( $form_data ) {
		if ( ! isset( $form_data ) ) {
			return $form_data;
		}

		if ( $form_data->has_error() ) {
			return $form_data;
		}

		try {
			$form_options = $form_data->get_wp_options();

			// The submission is already saved as a Record; the notification email is optional.
			if ( ! $form_options->has_email_notification() ) {
				return $form_data;
			}

			$email_subject = $form_options->has_email_subject() ? $form_options->get_email_subject() : ( __( 'A new form submission on ', 'otter-blocks' ) . get_bloginfo( 'name' ) );
			$email_subject = apply_filters( 'otter_form_email_build_subject', $email_subject, $form_data );

			$email_message = Form_Email::instance()->build_email( $form_data );
			$email_body    = apply_filters( 'otter_form_email_build_body', $email_message );

			// Sent the form date to the admin site as a default behavior.
			$to         = sanitize_email( get_site_option( 'admin_email' ) );
			$from_email = $to;

			// Check if we need to send it to another user email.
			if ( $form_data->payload_has( 'formOption' ) ) {
				$option_name = $form_data->get_data_from_payload( 'formOption' );
				$form_emails = get_option( 'themeisle_blocks_form_emails' );

				foreach ( $form_emails as $form ) {
					if ( isset( $form['form'] ) && $form['form'] === $option_name && isset( $form['email'] ) && '' !== $form['email'] ) {
						$to = sanitize_email( $form['email'] );
					}
				}

				if ( empty( $to ) ) {
					$to = sanitize_email( get_site_option( 'admin_email' ) );
				}

				if ( $form_options->has_from_email() && '' !== $form_options->get_from_email() ) {
					$from_email = sanitize_email( $form_options->get_from_email() );
				}
			}

			$headers = array( 'Content-Type: text/html', 'From: ' . ( $form_options->has_from_name() ? sanitize_text_field( $form_options->get_from_name() ) : get_bloginfo( 'name', 'display' ) ) . ' <' . $from_email . '>' );

			if ( ! empty( $form_options->get_cc() ) ) {
				$arr = explode( ',', $form_options->get_cc() );

				foreach ( $arr as $cc ) {
					$headers[] = 'Cc: ' . trim( $cc );
				}
			}

			if ( ! empty( $form_options->get_bcc() ) ) {
				$arr = explode( ',', $form_options->get_bcc() );

				foreach ( $arr as $cc ) {
					$headers[] = 'Bcc: ' . trim( $cc );
				}
			}

			// Replies go to the form submitter unless an explicit Reply-To is set.
			$reply_to = $form_options->has_reply_to() ? $form_options->get_reply_to() : $this->get_email_from_form_input( $form_data );

			if ( ! empty( $reply_to ) && is_email( $reply_to ) ) {
				$headers[] = 'Reply-To: ' . sanitize_email( $reply_to );
			}

			$attachments = array();
			if ( $form_data->has_uploaded_files() && ! $form_data->can_keep_uploaded_files() ) {
				foreach ( $form_data->get_uploaded_files_path() as $file ) {
					if ( empty( $file['file_location_slug'] ) ) {
						$attachments[] = $file['file_path'];
					}
				}
			}

			// phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.wp_mail_wp_mail
			$email_was_send = wp_mail( $to, $email_subject, $email_body, $headers, $attachments );
			if ( ! $email_was_send ) {
				$form_data->add_warning( Form_Data_Response::ERROR_EMAIL_NOT_SEND );
				$form_data->set_error( Form_Data_Response::ERROR_EMAIL_NOT_SEND );
			}
		} catch ( Exception  $e ) {
			$form_data->set_error( Form_Data_Response::ERROR_RUNTIME_ERROR );
			$form_data->add_warning( Form_Data_Response::ERROR_RUNTIME_ERROR, $e->getMessage() );
		}

		return $form_data;
	}

	/**
	 * Email the admin when an important error occurs.
	 *
	 * @param Form_Data_Request $form_data The form request data.
	 * @since 2.2.3
	 */
	public function send_error_email_to_admin( $form_data ) {
		if ( ! $form_data instanceof Form_Data_Request || $form_data->has_error() || $form_data->has_warning() ) {

			if ( ! $form_data instanceof Form_Data_Request ) {
				$form_data = new Form_Data_Request();
				$form_data->set_error( Form_Data_Response::ERROR_RUNTIME_ERROR, __( 'Some hook is corrupting the Form processing pipeline.', 'otter-blocks' ) );
			}

			$throttle_key        = '';
			$throttle_expiration = self::ADMIN_ALERT_EXPIRATION_TIME;

			if (
				$form_data->has_warning() &&
				$form_data->has_warning_codes(
					array(
						Form_Data_Response::ERROR_AUTORESPONDER_COULD_NOT_SEND,
						Form_Data_Response::ERROR_AUTORESPONDER_MISSING_EMAIL_FIELD,
					)
				)
			) {
				$key = $form_data->get_form_option_id() . '_autoresponder_error';

				if ( false === get_transient( $key ) ) {
					$throttle_key        = $key;
					$throttle_expiration = self::AUTO_RESPONDER_ERROR_EXPIRATION_TIME;
				}
			}

			if ( '' === $throttle_key && $form_data->has_infrastructure_failure() ) {
				$throttle_key = $this->get_available_alert_throttle_key( $form_data, 'captcha_provider' );
			}

			$delivery_failure_actions = Form_Submissions::get_delivery_failure_actions();

			if (
				'' === $throttle_key &&
				! $form_data->has_infrastructure_failure() &&
				(
					isset( $delivery_failure_actions[ $form_data->get_error_code() ] ) ||
					$form_data->has_warning_codes( array_keys( $delivery_failure_actions ) )
				)
			) {
				// Throttle each delivery action separately, so an alert for one action (e.g.: email) does not suppress the first alert for another (e.g.: webhook).
				$throttle_key = $this->get_available_alert_throttle_key( $form_data, 'delivery_' . $this->get_failed_delivery_action( $form_data, $delivery_failure_actions ) );
			}

			// Start the cooldown only after the alert was actually delivered, so a failed alert does not burn the throttle window.
			if ( '' !== $throttle_key && self::send_error_email( $form_data ) ) {
				set_transient( $throttle_key, true, $throttle_expiration );
			}
		}
	}

	/**
	 * Get the throttle key for an Admin Alert if its cooldown allows sending: at most one alert per form per failure type per hour.
	 *
	 * The caller must set the transient on the returned key only after the alert was successfully delivered.
	 *
	 * @param Form_Data_Request $form_data The form request data.
	 * @param string            $type The failure type (e.g.: 'captcha_provider', 'delivery_email').
	 * @return string The transient key for the alert, or an empty string when the alert is throttled.
	 * @since 3.1
	 */
	private function get_available_alert_throttle_key( $form_data, $type ) {
		$form_option_id = $form_data->get_form_option_id();

		if ( empty( $form_option_id ) ) {
			// Alerts without a form option id (e.g.: corrupted pipeline) get their own bucket instead of colliding with the per-form ones.
			$form_option_id = 'global';
		}

		$key = $form_option_id . '_alert_' . $type;

		if ( false !== get_transient( $key ) ) {
			return '';
		}

		return $key;
	}

	/**
	 * Get the delivery action whose failure triggered the Admin Alert.
	 *
	 * @param Form_Data_Request         $form_data The form request data.
	 * @param array<int|string, string> $delivery_failure_actions Map of response error codes to delivery actions.
	 * @return string The failed delivery action (e.g.: 'email', 'webhook', 'subscribe').
	 * @since 3.1
	 */
	private function get_failed_delivery_action( $form_data, $delivery_failure_actions ) {
		if ( isset( $delivery_failure_actions[ $form_data->get_error_code() ] ) ) {
			return $delivery_failure_actions[ $form_data->get_error_code() ];
		}

		foreach ( $form_data->get_warning_codes() as $warning ) {
			if ( isset( $warning['code'], $delivery_failure_actions[ $warning['code'] ] ) ) {
				return $delivery_failure_actions[ $warning['code'] ];
			}
		}

		return 'provider';
	}

	/**
	 * Logs the issues that occurred during the form submission.
	 *
	 * @param Form_Data_Request|null $form_data The form request data.
	 * @return void
	 */
	public function log_issues( $form_data ) {

		if ( ! defined( 'WP_DEBUG' ) || ! WP_DEBUG ) {
			return;
		}

		if ( ! $form_data instanceof Form_Data_Request ) {
			// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
			error_log( '[Otter Blocks][Form Block] The Form data is not a valid instance of Form_Data_Request.' );
			return;
		}

		if ( ! $form_data->has_warning() ) {
			return;
		}

		foreach ( $form_data->get_warning_codes() as $warning ) {
			$issue  = Form_Data_Response::get_error_code_message( $warning['code'] );
			$issue .= ! empty( $warning['details'] ) ? '(' . $warning['details'] . ')' : '';
			$issue .= '| Form data received: ' . wp_json_encode( $form_data->dump_data() );

			// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
			error_log( '[Otter Blocks][Form Block] Issue: ' . $issue );
		}
	}

	/**
	 * Make additional changes before using the main handler function for submitting.
	 *
	 * @param Form_Data_Request|null $form_data The form request data.
	 * @return Form_Data_Request|null
	 * @since 2.0.3
	 */
	public function change_provider_based_on_consent( $form_data ) {
		if ( ! isset( $form_data ) ) {
			return $form_data;
		}

		if ( $form_data->has_error() ) {
			return $form_data;
		}

		// If there is no consent, change the service to send only an email.
		if (
			'submit-subscribe' === $form_data->get_wp_options()->get_action() &&
			(
				! $form_data->payload_has( 'consent' ) ||
				! $form_data->get_data_from_payload( 'consent' )
			)
		) {
			$form_data->change_provider( 'default' );
		}

		return $form_data;
	}

	/**
	 * Process the extra actions after calling the main handler function for submitting.
	 *
	 * @param Form_Data_Request|null $form_data The form request data.
	 * @return void
	 * @since 2.0.3
	 */
	public function after_submit( $form_data ) {
		if ( ! isset( $form_data ) ) {
			return;
		}

		if ( $form_data->has_error() ) {
			return;
		}

		// No-consent subscribe requests fall back to the default provider, which already sends the owner email.
		if ( 'submit-subscribe' === $form_data->get_wp_options()->get_action() && 'default' === $form_data->get_changed_provider() ) {
			return;
		}

		// Send also an email to the form editor/owner with the data alongside the subscription.
		if (
			$form_data->get_wp_options()->has_provider() &&
			$form_data->get_wp_options()->has_list_id() &&
			(
				empty( $form_data->get_wp_options()->get_action() ) ||
				'submit-subscribe' === $form_data->get_wp_options()->get_action()
			)
		) {
			$this->send_default_email( $form_data );
		}
	}

	/**
	 * Check if the form was not completed by a bot.
	 *
	 * @param Form_Data_Request|null $form_data The form request data.
	 * @return Form_Data_Request|null
	 * @since 2.2.3
	 */
	public function anti_spam_validation( $form_data ) {
		if ( ! isset( $form_data ) ) {
			return $form_data;
		}

		if ( $form_data->has_error() ) {
			return $form_data;
		}

		if (
			$form_data->payload_has( 'antiSpamTime' ) &&
			is_numeric( $form_data->get_data_from_payload( 'antiSpamTime' ) ) &&
			$form_data->payload_has( 'antiSpamHoneyPot' )
		) {
			if (
				$form_data->get_data_from_payload( 'antiSpamTime' ) >= self::ANTI_SPAM_TIMEOUT &&
				'' === $form_data->get_data_from_payload( 'antiSpamHoneyPot' )
			) {
				return $form_data;
			}
		}

		$form_data->set_error( Form_Data_Response::ERROR_BOT_DETECTED );
		return $form_data;
	}

	/**
	 * Send an email about error, like: the integration api key is no longer valid.
	 *
	 * @param Form_Data_Request $form_data The form request data.
	 * @return bool Whether the email was sent.
	 * @since 2.0.3
	 */
	public static function send_error_email( $form_data ) {
		$email_subject = ( __( 'An error with the Form blocks has occurred on  ', 'otter-blocks' ) . get_bloginfo( 'name' ) );
		$email_message = Form_Email::instance()->build_error_email( $form_data );
		$email_body    = apply_filters( 'otter_form_email_build_body_error', $email_message );
		// Sent the form date to the admin site as a default behaviour.
		$to      = sanitize_email( get_site_option( 'admin_email' ) );
		$headers = array( 'Content-Type: text/html; charset=UTF-8', 'From: ' . esc_url( get_site_url() ) );
		// phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.wp_mail_wp_mail
		return wp_mail( $to, $email_subject, $email_body, $headers );
	}

	/**
	 * Send a test email.
	 *
	 * @param Form_Data_Request $form_data The test request.
	 * @return WP_Error|WP_HTTP_Response|WP_REST_Response
	 * @since 2.0.3
	 */
	public static function send_test_email( $form_data ) {
		$res = new Form_Data_Response();
		try {
			$email_subject = ( __( 'Test email for Otter Form from ', 'otter-blocks' ) . get_bloginfo( 'name', 'display' ) );
			$email_body    = Form_Email::instance()->build_test_email( $form_data );
			// Sent the form date to the admin site as a default behaviour.
			$to = sanitize_email( get_site_option( 'admin_email' ) );
			if ( $form_data->payload_has( 'to' ) && '' !== $form_data->get_data_from_payload( 'to' ) ) {
				$to = $form_data->get_data_from_payload( 'to' );
			}
			$headers = array( 'Content-Type: text/html; charset=UTF-8', 'From: ' . get_bloginfo( 'name', 'display' ) . '<' . $to . '>' );
			// phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.wp_mail_wp_mail
			$res->set_success( wp_mail( $to, $email_subject, $email_body, $headers ) );
		} catch ( Exception  $e ) {
			$res->set_code( Form_Data_Response::ERROR_RUNTIME_ERROR );
			$res->add_reason( $e->getMessage() );
		} finally {
			return $res->build_response();
		}
	}



	/**
	 * Get data from the given provider
	 *
	 * @param Form_Data_Request $form_data Search request.
	 * @return WP_Error|WP_HTTP_Response|WP_REST_Response
	 * @since 2.0.3
	 */
	public function get_integration_data( $form_data ) {
		$res = new Form_Data_Response();

		try {
			$service = null;
			switch ( $form_data->get_data_from_payload( 'provider' ) ) {
				case 'mailchimp':
					$service = new Mailchimp_Integration();
					break;
				case 'sendinblue':
					$service = new Sendinblue_Integration();
					break;
				default:
					$res->set_error( __( 'Invalid request! Provider is missing.', 'otter-blocks' ) );
			}

			if ( isset( $service ) ) {
				$valid_api_key = $service::validate_api_key( $form_data->get_data_from_payload( 'apiKey' ) );
				if ( $valid_api_key['valid'] ) {
					$service->set_api_key( $form_data->get_data_from_payload( 'apiKey' ) );
					$res->set_response( $service->get_information_from_provider( $form_data ) );
				} else {
					$res->set_code( $valid_api_key['code'] );
				}
			}
		} catch ( Exception $e ) {
			$res->set_code( Form_Data_Response::ERROR_RUNTIME_ERROR );
			$res->add_reason( $e->getMessage() );
		} finally {
			return $res->build_response();
		}
	}

	/**
	 * Test the subscription service.
	 *
	 * @param Form_Data_Request $form_data The test request.
	 * @return WP_Error|WP_HTTP_Response|WP_REST_Response
	 * @since 2.0.3
	 */
	public function test_subscription_service( $form_data ) {
		$res          = new Form_Data_Response();
		$form_options = Form_Settings_Data::get_form_setting_from_wordpress_options( $form_data->get_data_from_payload( 'formOption' ) );

		try {
			$service = null;
			switch ( $form_options->get_provider() ) {
				case 'mailchimp':
					$service = new Mailchimp_Integration();
					break;
				case 'sendinblue':
					$service = new Sendinblue_Integration();
					break;
				default:
					$res->set_code( Form_Data_Response::ERROR_PROVIDER_NOT_REGISTERED );
			}

			if ( isset( $service ) ) {
				$valid_api_key = $service::validate_api_key( $form_options->get_api_key() );
				if ( $valid_api_key['valid'] ) {
					if ( $form_options->has_list_id() ) {
						$service->set_api_key( $form_options->get_api_key() )->set_list_id( $form_options->get_list_id() );
						$response = $service->make_subscribe_request( Form_Utils::generate_test_email() );

						if ( is_wp_error( $response ) ) {
							$res->set_error( Form_Data_Response::ERROR_RUNTIME_ERROR, $response->get_error_message() );
						} else {
							$res->mark_as_success();
						}
					} else {
						$res->set_error( __( 'Contact list ID is missing!', 'otter-blocks' ) );
					}
				} else {
					$res->set_code( $valid_api_key['code'] );
				}
			}
		} catch ( Exception $e ) {
			$res->set_code( Form_Data_Response::ERROR_RUNTIME_ERROR );
			$res->add_reason( $e->getMessage() );
		} finally {
			return $res->build_response();
		}
	}

	/**
	 * Subscribe the user to a service.
	 *
	 * @param Form_Data_Request|null $form_data The form data.
	 * @return Form_Data_Request|null
	 * @since 2.0.3
	 */
	public function subscribe_to_service( $form_data ) {
		if ( ! isset( $form_data ) ) {
			return $form_data;
		}

		if ( $form_data->has_error() || $form_data->is_temporary() ) {
			return $form_data;
		}

		// Get the first email from form.
		$email = $form_data->get_first_email_from_input_fields();

		if ( '' === $email ) {
			$form_data->set_error( Form_Data_Response::ERROR_MISSING_EMAIL, __( 'Marketing Integration is active, but there is no Email field in the form. Please check your Form block settings in the page.', 'otter-blocks' ) );
			return $form_data;
		}

		if (
			'submit-subscribe' === $form_data->get_wp_options()->get_action() &&
			$form_data->payload_has( 'consent' ) &&
			! $form_data->get_data_from_payload( 'consent' )
		) {
			return $form_data;
		}

		try {
			// Get the api credentials from the Form block.
			$wp_options_form = $form_data->get_wp_options();

			$error_code = $wp_options_form->check_data();

			if (
				'' === $error_code
			) {
				$service = null;
				switch ( $wp_options_form->get_provider() ) {
					case 'mailchimp':
						$service = ( new Mailchimp_Integration() )->extract_data_from_integration( $wp_options_form );
						break;
					case 'sendinblue':
						$service = ( new Sendinblue_Integration() )->extract_data_from_integration( $wp_options_form );
						break;
				}

				$valid_api_key = $service::validate_api_key( $wp_options_form->get_api_key() );

				if ( $valid_api_key['valid'] ) {
					$form_data = $service->subscribe( $form_data );
				} else {
					$form_data->set_error( $valid_api_key['code'] );
				}
			} else {
				$form_data->set_error( $error_code );
			}
		} catch ( Exception $e ) {
			$form_data->set_error( Form_Data_Response::ERROR_RUNTIME_ERROR );
			$form_data->add_warning( Form_Data_Response::ERROR_RUNTIME_ERROR, $e->getMessage() );
		} finally {
			return $form_data;
		}
	}

	/**
	 * Check for required data.
	 *
	 * @param Form_Data_Request $form_data Data from the request.
	 *
	 * @return boolean
	 * @since 2.0.0
	 */
	public function has_required_data( $form_data ) {

		$main_fields_set = $form_data->are_root_data_set(
			array(
				'handler',
				'payload',
			)
		);

		$required_payload_fields = $form_data->are_payload_data_set(
			array(
				'nonceValue',
				'postUrl',
				'formId',
				'formOption',
			)
		);

		$is_nonce_valid = wp_verify_nonce( $form_data->get_data_from_payload( 'nonceValue' ), 'form-verification' );

		return $main_fields_set && $required_payload_fields && $is_nonce_valid;
	}

	/**
	 * Check if the data request has the data needed by form: captha, integrations.
	 *
	 * @access public
	 * @param Form_Data_Request|null $form_data Data from the request.
	 *
	 * @return Form_Data_Request|null
	 * @since 2.0.0
	 */
	public function check_form_conditions( $form_data ) {

		if ( ! isset( $form_data ) ) {
			return $form_data;
		}

		if ( $form_data->has_error() ) {
			return $form_data;
		}

		if ( ! $this->has_required_data( $form_data ) ) {
			$form_data->set_error( Form_Data_Response::ERROR_MISSING_DATA );
			return $form_data;
		}

		return $form_data;
	}

	/**
	 * Check if the data request has the data needed by form: captha, integrations.
	 *
	 * @access public
	 * @param Form_Data_Request|null $form_data Data from the request.
	 *
	 * @return Form_Data_Request|null
	 * @since 2.0.0
	 */
	public function check_form_captcha( $form_data ) {

		if ( ! isset( $form_data ) ) {
			return $form_data;
		}

		if ( $form_data->has_error() ) {
			return $form_data;
		}

		$form_options = $form_data->get_wp_options();

		if (
			$form_options->form_has_captcha() &&
			(
				! $form_data->payload_has( 'token' ) ||
				'' === $form_data->get_data_from_payload( 'token' )
			)
		) {
			$form_data->set_error( Form_Data_Response::ERROR_MISSING_CAPTCHA );
		}

		if ( $form_options->form_has_captcha() && $form_data->payload_has( 'token' ) && ! $form_data->has_error() ) {

			// The saved form options are authoritative; the payload value is only
			// a fallback for forms saved before the provider was stored with them.
			if ( $form_options->has_captcha_provider() ) {
				$provider = $form_options->get_captcha_provider();
			} else {
				$provider = $form_data->payload_has( 'captchaProvider' ) ? sanitize_key( $form_data->get_data_from_payload( 'captchaProvider' ) ) : 'recaptcha';
			}

			if ( 'turnstile' === $provider ) {
				$secret     = get_option( 'themeisle_cloudflare_turnstile_secret_key' );
				$verify_url = apply_filters( 'otter_blocks_turnstile_verify_url', 'https://challenges.cloudflare.com/turnstile/v0/siteverify' );
			} else {
				$secret     = get_option( 'themeisle_google_captcha_api_secret_key' );
				$verify_url = apply_filters( 'otter_blocks_recaptcha_verify_url', 'https://www.google.com/recaptcha/api/siteverify' );
			}

			if ( empty( $secret ) ) {
				$form_data->set_error( Form_Data_Response::ERROR_CAPTCHA_NOT_CONFIGURED );
				return $form_data;
			}

			$body = 'secret=' . rawurlencode( (string) $secret ) . '&response=' . rawurlencode( (string) $form_data->get_data_from_payload( 'token' ) );

			// phpcs:disable WordPressVIPMinimum.Variables.ServerVariables.UserControlledHeaders, WordPressVIPMinimum.Variables.RestrictedVariables.cache_constraints___SERVER__REMOTE_ADDR__
			if ( ! empty( $_SERVER['REMOTE_ADDR'] ) ) {
				$body .= '&remoteip=' . rawurlencode( sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) );
			}
			// phpcs:enable WordPressVIPMinimum.Variables.ServerVariables.UserControlledHeaders, WordPressVIPMinimum.Variables.RestrictedVariables.cache_constraints___SERVER__REMOTE_ADDR__

			$resp = wp_remote_post(
				$verify_url,
				array(
					'body'    => $body,
					'headers' => array(
						'Content-Type' => 'application/x-www-form-urlencoded',
					),
				)
			);

			/**
			 * A provider failure (unreachable service, timeout, 5xx) is not a verification failure:
			 * the submission is saved and marked failed instead of being rejected.
			 */
			$response_code = wp_remote_retrieve_response_code( $resp );
			if ( is_wp_error( $resp ) || 200 !== $response_code ) {
				$form_data->mark_infrastructure_failure(
					Form_Data_Response::ERROR_CAPTCHA_PROVIDER_UNREACHABLE,
					is_wp_error( $resp ) ? $resp->get_error_message() : 'HTTP ' . $response_code
				);
				return $form_data;
			}

			$result = json_decode( wp_remote_retrieve_body( $resp ), true );

			if ( ! is_array( $result ) || empty( $result['success'] ) ) {
				$form_data->set_error( Form_Data_Response::ERROR_INVALID_CAPTCHA_TOKEN );
			}
		}

		return $form_data;
	}

	/**
	 * Filter for the content of the email.
	 *
	 * @param string $content The content.
	 * @return string
	 * @since 2.0.3
	 */
	public function build_email_content( $content ) {
		return $content;
	}

	/**
	 * Filter for the content of the email for errors.
	 *
	 * @param string $content The content.
	 * @return string
	 * @sincee 2.0.3
	 */
	public function build_email_error_content( $content ) {
		return $content;
	}

	/**
	 * Get the first email from the input's form.
	 *
	 * @param Form_Data_Request $data The form data.
	 *
	 * @return mixed|string
	 * @since 2.0.3
	 */
	public function get_email_from_form_input( Form_Data_Request $data ) {
		$inputs = $data->get_data_from_payload( 'formInputsData' );
		if ( is_array( $inputs ) ) {
			foreach ( $inputs as $input_field ) {
				if ( isset( $input_field['type'] ) && 'email' == $input_field['type'] ) {
					return $input_field['value'];
				}
			}
		}
		return '';
	}

	/**
	 * Validate the input fields with files.
	 *
	 * @param Form_Data_Request|null $form_data The form data.
	 * @return Form_Data_Request|null
	 * @since 2.2.3
	 */
	public function check_form_files( $form_data ) {

		if ( ! isset( $form_data ) ) {
			return $form_data;
		}

		if ( $form_data->has_error() ) {
			return $form_data;
		}

		$inputs = $form_data->get_fields();

		foreach ( $inputs as $input ) {
			if ( Form_Utils::is_file_field( $input ) && ! Form_Utils::is_file_field_valid( $input ) ) {
				$form_data->set_error( Form_Data_Response::ERROR_FILES_METADATA_FORMAT );
				return $form_data;
			}
		}

		return $form_data;
	}

	/**
	 * Get the Field Options for the given Form.
	 *
	 * @param Form_Data_Request $form_data The form data.
	 * @since 2.2.3
	 */
	public static function pull_fields_options_for_form( $form_data ) {
		if ( ! ( $form_data instanceof Form_Data_Request ) || $form_data->has_error() ) {
			return $form_data;
		}

		$global_fields_options = get_option( 'themeisle_blocks_form_fields_option' );

		if ( empty( $global_fields_options ) ) {
			return $form_data;
		}

		foreach ( $form_data->get_fields() as $input ) {
			if ( isset( $input['metadata']['fieldOptionName'] ) ) {
				$field_name = $input['metadata']['fieldOptionName'];
				foreach ( $global_fields_options as $field ) {
					if ( isset( $field['fieldOptionName'] ) && $field['fieldOptionName'] === $field_name ) {
						$new_field = new Form_Field_WP_Option_Data( $field_name, $field['fieldOptionType'] );
						if ( isset( $field['options'] ) ) {
							$new_field->set_options( $field['options'] );
						}
						if ( isset( $field['stripe'] ) ) {
							$new_field->set_stripe_product_info( $field['stripe'] );
						}
						$form_data->add_field_wp_option( $new_field );
						break;
					}
				}
			}
		}

		$required_fields = $form_data->get_wp_options()->get_required_fields();

		foreach ( $required_fields as $required_field ) {
			foreach ( $global_fields_options as $field ) {
				if ( isset( $field['fieldOptionName'] ) && $field['fieldOptionName'] === $required_field ) {
					$new_field = new Form_Field_WP_Option_Data( $required_field, $field['fieldOptionType'] );
					if ( isset( $field['options'] ) ) {
						$new_field->set_options( $field['options'] );
					}
					if ( isset( $field['stripe'] ) ) {
						$new_field->set_stripe_product_info( $field['stripe'] );
					}
					$form_data->add_field_wp_option( $new_field );
					break;
				}
			}
		}

		return $form_data;
	}

	/**
	 * Verify the given confirmation session.
	 *
	 * @param string $session The session id.
	 * @return bool
	 */
	public function verify_confirmation_session( $session ) {
		return ! empty( $session ) && is_string( $session );
	}
	/**
	 * The instance method for the static class.
	 * Defines and returns the instance of the static class.
	 *
	 * @static
	 * @since 1.0.0
	 * @access public
	 * @return Form_Server
	 */
	public static function instance() {
		if ( is_null( self::$instance ) ) {
			self::$instance = new self();
			self::$instance->init();
		}

		return self::$instance;
	}

	/**
	 * Throw error on object clone
	 *
	 * The whole idea of the singleton design pattern is that there is a single
	 * object therefore, we don't want the object to be cloned.
	 *
	 * @access public
	 * @return void
	 * @since 2.0.0
	 */
	public function __clone() {
		// Cloning instances of the class is forbidden.
		_doing_it_wrong( __FUNCTION__, esc_html__( 'Cheatin&#8217; huh?', 'otter-blocks' ), '1.0.0' );
	}

	/**
	 * Disable unserializing of the class
	 *
	 * @access public
	 * @return void
	 * @since 2.0.0
	 */
	public function __wakeup() {
		// Unserializing instances of the class is forbidden.
		_doing_it_wrong( __FUNCTION__, esc_html__( 'Cheatin&#8217; huh?', 'otter-blocks' ), '1.0.0' );
	}
}
