<?php
/**
 * Signup Block
 *
 * Provides a block for ambassadors to sign up and log in.
 *
 * @package AmbassadorHub\Accounts\Blocks\Signup
 */

namespace AmbassadorHub\Accounts\Blocks\Signup;

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

use AmbassadorHub\Accounts\AccountFields;
use AmbassadorHub\Accounts\Admin\Settings\SettingsManager;
use AmbassadorHub\Spam\SpamShield;
use AmbassadorHub\Support\RequestData;

/**
 * Signup 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_ambassador_signup', array( self::class, 'handle_signup' ) );
		add_action( 'admin_post_ambh_ambassador_signup', array( self::class, 'handle_signup' ) );
		add_action( 'admin_post_nopriv_ambh_ambassador_login', array( self::class, 'handle_login' ) );
		add_action( 'admin_post_ambh_ambassador_login', array( self::class, 'handle_login' ) );
	}

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

		wp_register_script(
			$handle,
			$block_url . 'index.js',
			array( 'wp-blocks', 'wp-i18n', 'wp-element', 'wp-server-side-render' ),
			filemtime( $block_dir . 'index.js' ),
			true
		);

		register_block_type(
			$block_dir,
			array(
				'editor_script'   => $handle,
				'render_callback' => array( self::class, 'render_block' ),
			)
		);

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

	/**
	 * Render the block.
	 *
	 * @param array $attributes Block attributes.
	 *
	 * @return string Block HTML.
	 */
	public static function render_block( array $attributes = array() ): string { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found -- required by block render_callback signature.
		// If user is logged in, never show the login/signup form — doing so
		// creates a redirect loop when no account page is configured because
		// handle_login() would redirect back to home_url('/') which may itself
		// contain this block.
		if ( is_user_logged_in() ) {
			if ( SettingsManager::is_account_page_configured() ) {
				// translators: %1$s: "You are already logged in." text, %2$s: URL of account portal, %3$s: link label.
				return sprintf(
					'<p>%s <a href="%s">%s</a></p>',
					esc_html__( 'You are already logged in.', 'ambassadorhub' ),
					esc_url( SettingsManager::get_account_page_url() ),
					esc_html__( 'Go to your account portal', 'ambassadorhub' )
				);
			}

			// Account page not configured: still stop rendering the form so we
			// don't loop. Just confirm the user is already logged in.
			return '<p>' . esc_html__( 'You are already logged in.', 'ambassadorhub' ) . '</p>';
		}

		$action_signup = esc_url( admin_url( 'admin-post.php?action=ambh_ambassador_signup' ) );
		$action_login  = esc_url( admin_url( 'admin-post.php?action=ambh_ambassador_login' ) );

		// Carry the redirect_to param so handlers can return the user to the right
		// page (e.g. the fundraiser page they were trying to reach) after login.
		$redirect_to = isset( $_GET['redirect_to'] ) // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only display; validated in the handler before use.
			? sanitize_url( wp_unslash( $_GET['redirect_to'] ) ) // phpcs:ignore WordPress.Security.NonceVerification.Recommended
			: '';

		// Capture any error surfaced by the form handler via query param.
		$error_code  = isset( $_GET['ambh_error'] ) ? sanitize_key( $_GET['ambh_error'] ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
		$error_form  = isset( $_GET['ambh_form'] ) ? sanitize_key( $_GET['ambh_form'] ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
		$verif_param = isset( $_GET['ambh_verification'] ) ? sanitize_key( $_GET['ambh_verification'] ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended

		// Pre-fill values: set by the invite link or echoed back after a failed submission.
		$prefill_first_name = isset( $_GET['ambh_fn'] ) ? sanitize_text_field( wp_unslash( $_GET['ambh_fn'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
		$prefill_last_name  = isset( $_GET['ambh_ln'] ) ? sanitize_text_field( wp_unslash( $_GET['ambh_ln'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
		$prefill_email      = isset( $_GET['ambh_email'] ) ? sanitize_email( wp_unslash( $_GET['ambh_email'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended

		ob_start();
		$wrapper_attributes = get_block_wrapper_attributes( array( 'class' => 'ambh-auth-columns' ) );
		?>
		<div <?php echo $wrapper_attributes; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- sanitized by get_block_wrapper_attributes(). ?>>
		<?php if ( 'success' === $verif_param ) : ?>
			<div class="ambh-auth-notice ambh-auth-notice--success">
				<p><?php esc_html_e( 'Your email address has been verified successfully. You can now log in below.', 'ambassadorhub' ); ?></p>
			</div>
		<?php elseif ( 'failed' === $verif_param ) : ?>
			<div class="ambh-auth-notice ambh-auth-notice--error">
				<p><?php esc_html_e( 'The verification link is invalid or has expired. Log in and request a new verification email from your account dashboard.', 'ambassadorhub' ); ?></p>
			</div>
		<?php endif; ?>
			<div class="ambh-auth-box">
				<h3><?php echo esc_html__( 'Register as Ambassador', 'ambassadorhub' ); ?></h3>
				<?php if ( $error_code && 'signup' === $error_form ) : ?>
					<div class="ambh-auth-error"><?php echo esc_html( self::get_error_message( $error_code ) ); ?></div>
				<?php endif; ?>
				<form method="post" action="<?php echo esc_url( $action_signup ); ?>">
					<?php wp_nonce_field( 'ambh_ambassador_signup', '_ambh_nonce' ); ?>
					<?php if ( $redirect_to ) : ?>
						<input type="hidden" name="redirect_to" value="<?php echo esc_attr( $redirect_to ); ?>" />
					<?php endif; ?>
					<p>
						<label for="first_name"><?php echo esc_html__( 'First name', 'ambassadorhub' ); ?></label>
						<input type="text" name="first_name" id="first_name" required value="<?php echo esc_attr( $prefill_first_name ); ?>" />
					</p>
					<p>
						<label for="last_name"><?php echo esc_html__( 'Last name', 'ambassadorhub' ); ?></label>
						<input type="text" name="last_name" id="last_name" required value="<?php echo esc_attr( $prefill_last_name ); ?>" />
					</p>
					<?php AccountFields::render_signup_fields(); ?>
					<p>
						<label for="email"><?php echo esc_html__( 'Email', 'ambassadorhub' ); ?></label>
						<input type="email" name="email" id="email" required value="<?php echo esc_attr( $prefill_email ); ?>" />
					</p>
					<p>
						<label for="ambh-signup-password"><?php echo esc_html__( 'Password', 'ambassadorhub' ); ?></label>
						<input type="password" name="password" id="ambh-signup-password" required autocomplete="new-password" />
					</p>
					<?php do_action( 'ambh_render_registration_form_fields' ); ?>
					<?php SpamShield::render_protection_fields( 'signup' ); ?>
					<p>
						<button type="submit" class="ambh-auth-button ambh-auth-button--primary"><?php echo esc_html__( 'Sign up', 'ambassadorhub' ); ?></button>
					</p>
				</form>
			</div>
			<div class="ambh-auth-box" id="ambh-auth-box-login">
				<h3><?php echo esc_html__( 'Login', 'ambassadorhub' ); ?></h3>
				<?php if ( $error_code && 'login' === $error_form ) : ?>
					<div class="ambh-auth-error"><?php echo esc_html( self::get_error_message( $error_code ) ); ?></div>
				<?php endif; ?>
				<form method="post" action="<?php echo esc_url( $action_login ); ?>">
					<?php wp_nonce_field( 'ambh_ambassador_login', '_ambh_nonce' ); ?>
					<?php if ( $redirect_to ) : ?>
						<input type="hidden" name="redirect_to" value="<?php echo esc_attr( $redirect_to ); ?>" />
					<?php endif; ?>
					<p>
						<label><?php echo esc_html__( 'Email', 'ambassadorhub' ); ?></label>
						<input type="email" name="email" required />
					</p>
					<p>
						<label><?php echo esc_html__( 'Password', 'ambassadorhub' ); ?></label>
						<input type="password" name="password" required />
					</p>
					<p>
						<button type="submit" class="ambh-auth-button"><?php echo esc_html__( 'Login', 'ambassadorhub' ); ?></button>
					</p>
				</form>
			</div>
		</div>
		<?php
		return (string) ob_get_clean();
	}

	/**
	 * Map an ambh_error code to a human-readable message.
	 *
	 * @param string $code Error code from the query string.
	 *
	 * @return string Translated error message.
	 */
	private static function get_error_message( string $code ): string {
		$messages = array(
			'invalid_nonce'  => __( 'Security check failed, please try again.', 'ambassadorhub' ),
			'missing_fields' => __( 'Please fill in all required fields.', 'ambassadorhub' ),
			'weak_password'  => __( 'Your password must be at least 8 characters and contain an uppercase letter, a number, and a special character.', 'ambassadorhub' ),
			'email_exists'   => __( 'This email address is already registered. Please log in instead.', 'ambassadorhub' ),
			'email_blocked'  => __( 'This email address cannot be used to register. Please use a different address.', 'ambassadorhub' ),
			'create_failed'  => __( 'Could not create your account. Please try again.', 'ambassadorhub' ),
			'login_failed'   => __( 'Invalid email or password. Please try again.', 'ambassadorhub' ),
			'rate_limited'   => __( 'Too many signup attempts. Please wait a while and try again.', 'ambassadorhub' ),
			'spam_detected'  => __( 'Your submission looked automated and was blocked. Please try again.', 'ambassadorhub' ),
		);

		return $messages[ $code ] ?? __( 'Something went wrong. Please try again.', 'ambassadorhub' );
	}

	/**
	 * Render shortcode.
	 *
	 * @param array $atts Shortcode attributes.
	 *
	 * @return string
	 */
	public static function render_shortcode( array $atts = array() ): string {
		return self::render_block( $atts );
	}

	/**
	 * Handle ambassador signup form submission.
	 *
	 * @return void
	 */
	public static function handle_signup(): void {
		// Verify nonce.
		$nonce = isset( $_POST['_ambh_nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['_ambh_nonce'] ) ) : '';
		if ( ! wp_verify_nonce( $nonce, 'ambh_ambassador_signup' ) ) {
			$referer = wp_get_referer();
			wp_safe_redirect(
				add_query_arg(
					array(
						'ambh_error' => 'invalid_nonce',
						'ambh_form'  => 'signup',
					),
					$referer ? $referer : home_url( '/' )
				)
			);
			exit;
		}

		// Rate limit + honeypot/timing check — reject likely-automated submissions
		// before doing any real work. See AmbassadorHub\Spam\SpamShield.
		if ( ! SpamShield::throttle_by_ip( 'signup', SpamShield::signup_rate_limit(), HOUR_IN_SECONDS ) ) {
			wp_safe_redirect(
				add_query_arg(
					array(
						'ambh_error' => 'rate_limited',
						'ambh_form'  => 'signup',
					),
					wp_get_referer() ? wp_get_referer() : home_url( '/' )
				)
			);
			exit;
		}
		if ( ! SpamShield::passed( 'signup' ) ) {
			wp_safe_redirect(
				add_query_arg(
					array(
						'ambh_error' => 'spam_detected',
						'ambh_form'  => 'signup',
					),
					wp_get_referer() ? wp_get_referer() : home_url( '/' )
				)
			);
			exit;
		}

		// Read and validate the optional post-login redirect target.
		$redirect_to = self::get_validated_redirect_to();

		$first_name = isset( $_POST['first_name'] ) ? sanitize_text_field( wp_unslash( $_POST['first_name'] ) ) : '';
		$last_name  = isset( $_POST['last_name'] ) ? sanitize_text_field( wp_unslash( $_POST['last_name'] ) ) : '';
		$email      = isset( $_POST['email'] ) ? sanitize_email( wp_unslash( $_POST['email'] ) ) : '';
		$password   = isset( $_POST['password'] ) ? (string) wp_unslash( $_POST['password'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- passwords must not be sanitized as it would alter the value.

		// Helper: build error redirect preserving redirect_to and entered values so the form can re-show them.
		$referer    = wp_get_referer();
		$referer    = $referer ? $referer : home_url( '/' );
		$error_base = $redirect_to
			? add_query_arg( 'redirect_to', rawurlencode( $redirect_to ), $referer )
			: $referer;
		$error_base = add_query_arg(
			array(
				'ambh_form'  => 'signup',
				'ambh_fn'    => $first_name,
				'ambh_ln'    => $last_name,
				'ambh_email' => $email,
			),
			$error_base
		);

		if ( ! self::is_password_strong_enough( $password ) ) {
			wp_safe_redirect( add_query_arg( 'ambh_error', 'weak_password', $error_base ) );
			exit;
		}

		if ( empty( $email ) || empty( $password ) ) {
			wp_safe_redirect( add_query_arg( 'ambh_error', 'missing_fields', $error_base ) );
			exit;
		}

		if ( email_exists( $email ) ) {
			wp_safe_redirect( add_query_arg( 'ambh_error', 'email_exists', $error_base ) );
			exit;
		}

		if ( SpamShield::is_blocked_email( $email ) ) {
			SpamShield::log( 'signup', 'blocked_email' );
			wp_safe_redirect( add_query_arg( 'ambh_error', 'email_blocked', $error_base ) );
			exit;
		}

		// Generate a unique username derived from the email local-part.
		$username = self::generate_unique_username( $email );

		// Create user.
		$user_id = wp_create_user( $username, $password, $email );
		if ( is_wp_error( $user_id ) ) {
			wp_safe_redirect( add_query_arg( 'ambh_error', 'create_failed', $error_base ) );
			exit;
		}

		// Update profile and set role.
		wp_update_user(
			array(
				'ID'         => $user_id,
				'first_name' => $first_name,
				'last_name'  => $last_name,
				'role'       => \AmbassadorHub\Accounts\Roles::ROLE_AMBASSADOR,
			)
		);

		AccountFields::save_from_request( $user_id, $_POST, 'signup' );
		AccountFields::maybe_update_display_name( $user_id );

		do_action(
			'ambh_successful_signup_from_submission',
			$user_id,
			RequestData::sanitized_copy( $_POST, array( 'password', '_ambh_nonce' ) ) // phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce verified above; excluded from the copy along with the plaintext password.
		);

		// Sign on and redirect.
		$user = wp_signon(
			array(
				'user_login'    => $username,
				'user_password' => $password,
				'remember'      => true,
			),
			false
		);

		$success_url = $redirect_to ? $redirect_to : self::get_account_url();
		wp_safe_redirect( is_wp_error( $user ) ? add_query_arg( 'ambh_error', 'login_failed', $error_base ) : $success_url );
		exit;
	}

	/**
	 * Handle ambassador login form submission.
	 *
	 * @return void
	 */
	public static function handle_login(): void {
		$nonce = isset( $_POST['_ambh_nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['_ambh_nonce'] ) ) : '';
		if ( ! wp_verify_nonce( $nonce, 'ambh_ambassador_login' ) ) {
			$referer = wp_get_referer();
			wp_safe_redirect(
				add_query_arg(
					array(
						'ambh_error' => 'invalid_nonce',
						'ambh_form'  => 'login',
					),
					$referer ? $referer : home_url( '/' )
				)
			);
			exit;
		}

		// Read and validate the optional post-login redirect target.
		$redirect_to = self::get_validated_redirect_to();

		$email    = isset( $_POST['email'] ) ? sanitize_email( wp_unslash( $_POST['email'] ) ) : '';
		$password = isset( $_POST['password'] ) ? (string) wp_unslash( $_POST['password'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- passwords must not be sanitized as it would alter the value.

		// Helper: build error redirect preserving redirect_to so the form can re-show it.
		$referer    = wp_get_referer();
		$referer    = $referer ? $referer : home_url( '/' );
		$error_base = $redirect_to
			? add_query_arg( 'redirect_to', rawurlencode( $redirect_to ), $referer )
			: $referer;
		$error_base = add_query_arg( 'ambh_form', 'login', $error_base );

		if ( empty( $email ) || empty( $password ) ) {
			wp_safe_redirect( add_query_arg( 'ambh_error', 'missing_fields', $error_base ) );
			exit;
		}

		$user_obj = get_user_by( 'email', $email );
		$username = $user_obj ? $user_obj->user_login : $email; // fallback if login == email

		$user = wp_signon(
			array(
				'user_login'    => $username,
				'user_password' => $password,
				'remember'      => true,
			),
			false
		);

		$success_url = $redirect_to ? $redirect_to : self::get_account_url();
		wp_safe_redirect( is_wp_error( $user ) ? add_query_arg( 'ambh_error', 'login_failed', $error_base ) : $success_url );
		exit;
	}

	/**
	 * Check whether a password meets the minimum complexity requirements.
	 *
	 * Rules (mirror the client-side strength meter):
	 *  - at least 8 characters
	 *  - at least one uppercase letter [A-Z]
	 *  - at least one digit [0-9]
	 *  - at least one non-alphanumeric character
	 *
	 * @param string $password Plain-text password (not sanitized — must not be altered).
	 *
	 * @return bool
	 */
	private static function is_password_strong_enough( string $password ): bool {
		return strlen( $password ) >= 8
			&& preg_match( '/[A-Z]/', $password )
			&& preg_match( '/[0-9]/', $password )
			&& preg_match( '/[^A-Za-z0-9]/', $password );
	}

	/**
	 * Read and validate the redirect_to POST param.
	 *
	 * Uses wp_validate_redirect() to guarantee the target is on the same host,
	 * preventing open-redirect attacks. Returns an empty string when absent or
	 * when the value points off-site.
	 *
	 * @return string Validated local URL, or empty string.
	 */
	private static function get_validated_redirect_to(): string {
		if ( empty( $_POST['redirect_to'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce verified by caller before this helper is called.
			return '';
		}
		$raw = sanitize_url( wp_unslash( $_POST['redirect_to'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
		return (string) wp_validate_redirect( $raw, '' );
	}

	/**
	 * Generate a unique WordPress username from an email address.
	 *
	 * Takes the local-part of the email as a base, sanitizes it with
	 * sanitize_user( $str, true ) (strict mode strips everything WordPress
	 * does not allow in a username), then appends an incrementing numeric
	 * suffix until an unused slot is found.
	 *
	 * @param string $email Email address to derive the username from.
	 *
	 * @return string A username that does not yet exist in the database.
	 */
	private static function generate_unique_username( string $email ): string {
		$base = sanitize_user( explode( '@', $email )[0], true );

		// Fall back when the local-part contained no usable characters.
		if ( '' === $base ) {
			$base = 'ambassador';
		}

		$username     = $base;
		$suffix       = 1;
		$max_attempts = 100;

		while ( username_exists( $username ) ) {
			if ( $suffix > $max_attempts ) {
				// Fall back to a random suffix to break out of any unlikely deadlock.
				$username = $base . wp_generate_password( 6, false, false );
				break;
			}
			$username = $base . $suffix;
			++$suffix;
		}

		return $username;
	}

	/**
	 * Get the account/portal URL.
	 *
	 * Uses the configured account page or falls back to home URL.
	 * Allows filtering via 'ambh_account_url' hook.
	 *
	 * @return string Account URL.
	 */
	private static function get_account_url(): string {
		return apply_filters( 'ambh_account_url', SettingsManager::get_account_page_url() );
	}
}
