<?php
/**
 * Frontend Login Form feature for Guard Dog.
 *
 * @package GuardDog
 * @since 1.9.39
 */

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

class Guard_Dog_Login_Form {

	/**
	 * Block name for the frontend login form.
	 *
	 * @var string
	 */
	const BLOCK_NAME = 'guard-dog/login-form';

	/**
	 * Editor script handle for the login form block.
	 *
	 * @var string
	 */
	const BLOCK_EDITOR_SCRIPT = 'guard-dog-login-form-block';

	/**
	 * Shortcode tag for the frontend login form.
	 *
	 * @var string
	 */
	const SHORTCODE = 'guard_dog_login_form';

	/**
	 * The hidden POST action field for shortcode submissions.
	 *
	 * @var string
	 */
	const SUBMIT_ACTION = 'guard_dog_frontend_login';

	/**
	 * Shortcode submission nonce action.
	 *
	 * @var string
	 */
	const NONCE_ACTION = 'guard_dog_login_form_submit';

	/**
	 * Minimum custom width allowed for the frontend login form.
	 *
	 * @var int
	 */
	const MAX_WIDTH_MIN = 280;

	/**
	 * Maximum custom width allowed for the frontend login form.
	 *
	 * @var int
	 */
	const MAX_WIDTH_MAX = 960;

	/**
	 * Captured submission error from the current request.
	 *
	 * @var WP_Error|null
	 */
	private $submission_error = null;

	/**
	 * Last submitted username/email.
	 *
	 * @var string
	 */
	private $submitted_login = '';

	/**
	 * Constructor.
	 */
	public function __construct() {
		$this->add_hooks();
	}

	/**
	 * Register feature hooks.
	 */
	private function add_hooks() {
		add_shortcode( self::SHORTCODE, array( $this, 'shortcode_handler' ) );
		add_action( 'template_redirect', array( $this, 'maybe_handle_login_submission' ), 1 );
		add_action( 'init', array( $this, 'register_block' ) );
	}

	/**
	 * Render the frontend login form shortcode.
	 *
	 * @param array|string $atts Shortcode attributes.
	 * @return string
	 */
	public function shortcode_handler( $atts ) {
		if ( ! $this->is_frontend_login_submission_request() ) {
			$this->reset_submission_state();
		}

		$atts               = $this->normalize_atts( $atts );
		$redirect_to        = $this->resolve_redirect_to( $atts );
		$error_return_url   = $this->get_error_return_url();
		$is_logged_in       = is_user_logged_in();
		$continue_url       = $this->get_logged_in_continue_url( $redirect_to );
		$current_user       = wp_get_current_user();
		$error_messages     = $this->get_submission_error_messages();
		$passkey_markup     = ( ! $is_logged_in ) ? $this->get_passkey_markup( $atts ) : '';
		$social_login_markup = ( ! $is_logged_in ) ? $this->get_social_login_markup( $atts, $redirect_to, $error_return_url ) : '';

		$this->enqueue_assets( $atts, $is_logged_in );

		return Guard_Dog_View_Loader::load_partial(
			'login-form',
			array(
				'atts'             => $atts,
				'is_logged_in'     => $is_logged_in,
				'action_url'       => $this->get_form_action_url(),
				'redirect_to'      => $redirect_to,
				'continue_url'     => $continue_url,
				'current_user'     => $current_user,
				'login_value'      => $this->submitted_login,
				'error_messages'   => $error_messages,
				'inline_style'     => $this->get_wrapper_inline_style( $atts ),
				'passkey_markup'   => $passkey_markup,
				'social_login_markup' => $social_login_markup,
				'captcha_markup'   => $this->get_captcha_markup(),
				'lost_password_url'=> $atts['show_lost_password'] ? wp_lostpassword_url( $redirect_to ) : '',
			)
		);
	}

	/**
	 * Handle frontend login form POSTs before output begins.
	 */
	public function maybe_handle_login_submission() {
		$this->reset_submission_state();

		if ( is_admin() || wp_doing_ajax() ) {
			return;
		}

		$request_method = isset( $_SERVER['REQUEST_METHOD'] ) ? strtoupper( sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) ) : 'GET';
		if ( 'POST' !== $request_method ) {
			return;
		}

		// phpcs:disable WordPress.Security.NonceVerification.Missing
		$submitted_action = isset( $_POST[ self::SUBMIT_ACTION ] ) ? sanitize_text_field( wp_unslash( $_POST[ self::SUBMIT_ACTION ] ) ) : '';
		// phpcs:enable WordPress.Security.NonceVerification.Missing
		if ( '1' !== $submitted_action ) {
			return;
		}

		if ( is_user_logged_in() ) {
			$this->redirect_to( $this->get_redirect_to_from_post() );
		}

		// phpcs:disable WordPress.Security.NonceVerification.Missing
		$this->submitted_login = isset( $_POST['log'] ) ? sanitize_text_field( wp_unslash( $_POST['log'] ) ) : '';
		$password              = isset( $_POST['pwd'] ) ? (string) wp_unslash( $_POST['pwd'] ) : '';
		$remember              = ! empty( $_POST['rememberme'] );
		$nonce                 = isset( $_POST['guard_dog_login_form_nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['guard_dog_login_form_nonce'] ) ) : '';
		// phpcs:enable WordPress.Security.NonceVerification.Missing

		if ( ! wp_verify_nonce( $nonce, self::NONCE_ACTION ) ) {
			$this->submission_error = new WP_Error(
				'guard_dog_login_form_invalid_nonce',
				__( 'Your login session expired. Please refresh the page and try again.', 'guard-dog' )
			);
			return;
		}

		if ( '' === $this->submitted_login || '' === $password ) {
			$this->submission_error = new WP_Error(
				'guard_dog_login_form_missing_fields',
				__( 'Please enter your username or email address and your password.', 'guard-dog' )
			);
			return;
		}

		$user = wp_signon(
			array(
				'user_login'    => $this->submitted_login,
				'user_password' => $password,
				'remember'      => $remember,
			),
			is_ssl()
		);

		if ( is_wp_error( $user ) ) {
			$this->submission_error = $user;
			return;
		}

		$redirect_to = $this->get_redirect_to_from_post();
		if ( function_exists( 'Guard_Dog' ) && isset( Guard_Dog()->features['passkeys'] ) ) {
			$passkeys = Guard_Dog()->features['passkeys'];
			if ( is_object( $passkeys ) && method_exists( $passkeys, 'get_password_login_setup_prompt_redirect' ) ) {
				$redirect_to = $passkeys->get_password_login_setup_prompt_redirect(
					$user,
					$redirect_to,
					$this->has_passkey_setup_intent_from_post()
				);
			}
		}

		wp_set_current_user( $user->ID );
		$this->redirect_to( $redirect_to );
	}

	/**
	 * Parse and normalize shortcode attributes.
	 *
	 * @param array|string $atts Raw shortcode attributes.
	 * @return array
	 */
	private function normalize_atts( $atts ) {
		$atts     = is_array( $atts ) ? $atts : array();
		$defaults = $this->get_default_settings();

		return array(
			'style'              => $this->normalize_style( $this->get_atts_value( $atts, 'style', $defaults['style'] ) ),
			'redirect_to'        => $this->normalize_redirect_to( $this->get_atts_value( $atts, 'redirect_to', '' ) ),
			'show_passkey'       => $this->to_bool( $this->get_atts_value( $atts, 'show_passkey', $defaults['show_passkey'] ) ),
			'show_social_login'  => $this->to_bool( $this->get_atts_value( $atts, 'show_social_login', $defaults['show_social_login'] ) ),
			'show_remember_me'   => $this->to_bool( $this->get_atts_value( $atts, 'show_remember_me', $defaults['show_remember_me'] ) ),
			'show_lost_password' => $this->to_bool( $this->get_atts_value( $atts, 'show_lost_password', $defaults['show_lost_password'] ) ),
			'max_width'          => $this->normalize_max_width( $this->get_atts_value( $atts, 'max_width', $defaults['max_width'] ) ),
		);
	}

	/**
	 * Register the Gutenberg block wrapper for the frontend login form.
	 */
	public function register_block() {
		if ( ! function_exists( 'register_block_type' ) || ! class_exists( '\WP_Block_Type_Registry' ) ) {
			return;
		}

		$registry = \WP_Block_Type_Registry::get_instance();
		if ( $registry->is_registered( self::BLOCK_NAME ) ) {
			return;
		}

		$this->register_block_editor_assets();

		register_block_type(
			self::BLOCK_NAME,
			array(
				'api_version'     => 2,
				'editor_script'   => self::BLOCK_EDITOR_SCRIPT,
				'render_callback' => array( $this, 'render_block' ),
				'attributes'      => $this->get_block_attributes_schema(),
			)
		);
	}

	/**
	 * Register the editor-side block assets.
	 */
	public function register_block_editor_assets() {
		if ( ! wp_script_is( self::BLOCK_EDITOR_SCRIPT, 'registered' ) ) {
			wp_register_script(
				self::BLOCK_EDITOR_SCRIPT,
				GUARD_DOG_URL . 'assets/js/guard-dog-login-form-block.js',
				array( 'wp-blocks', 'wp-element', 'wp-components', 'wp-i18n', 'wp-block-editor' ),
				GUARD_DOG_VERSION,
				true
			);
		}

		$defaults = $this->get_default_settings();

		wp_localize_script(
			self::BLOCK_EDITOR_SCRIPT,
			'guardDogLoginFormBlockSettings',
			array(
				'defaults'    => array(
					'style'              => $defaults['style'],
					'show_passkey'       => $defaults['show_passkey'],
					'show_social_login'  => $defaults['show_social_login'],
					'show_remember_me'   => $defaults['show_remember_me'],
					'show_lost_password' => $defaults['show_lost_password'],
					'max_width'          => '' === $defaults['max_width'] ? '' : (string) $defaults['max_width'],
				),
				'maxWidthMin' => self::MAX_WIDTH_MIN,
				'maxWidthMax' => self::MAX_WIDTH_MAX,
			)
		);
	}

	/**
	 * Render the login form block using the canonical shortcode path.
	 *
	 * @param array  $attributes Block attributes.
	 * @param string $content    Block content.
	 * @return string
	 */
	public function render_block( $attributes = array(), $content = '' ) {
		unset( $content );

		return $this->shortcode_handler( (array) $attributes );
	}

	/**
	 * Return the block attribute schema for the login form block.
	 *
	 * @return array
	 */
	private function get_block_attributes_schema() {
		$defaults = $this->get_default_settings();

		return array(
			'style'             => array(
				'type'    => 'string',
				'default' => $defaults['style'],
			),
			'redirect_to'       => array(
				'type'    => 'string',
				'default' => '',
			),
			'show_passkey'      => array(
				'type'    => 'boolean',
				'default' => $defaults['show_passkey'],
			),
			'show_social_login' => array(
				'type'    => 'boolean',
				'default' => $defaults['show_social_login'],
			),
			'show_remember_me'  => array(
				'type'    => 'boolean',
				'default' => $defaults['show_remember_me'],
			),
			'show_lost_password'=> array(
				'type'    => 'boolean',
				'default' => $defaults['show_lost_password'],
			),
			'max_width'         => array(
				'type'    => 'string',
				'default' => '' === $defaults['max_width'] ? '' : (string) $defaults['max_width'],
			),
		);
	}

	/**
	 * Return the saved defaults for new frontend login form instances.
	 *
	 * @return array
	 */
	private function get_default_settings() {
		return array(
			'style'              => $this->normalize_style( get_option( 'guard_dog_login_form_style', 'default' ) ),
			'show_passkey'       => $this->to_bool( get_option( 'guard_dog_login_form_show_passkey', true ) ),
			'show_social_login'  => $this->to_bool( get_option( 'guard_dog_login_form_show_social_login', true ) ),
			'show_remember_me'   => $this->to_bool( get_option( 'guard_dog_login_form_show_remember_me', true ) ),
			'show_lost_password' => $this->to_bool( get_option( 'guard_dog_login_form_show_lost_password', true ) ),
			'max_width'          => $this->normalize_max_width( get_option( 'guard_dog_login_form_max_width', '' ) ),
		);
	}

	/**
	 * Return an explicit attribute value or a fallback default.
	 *
	 * @param array  $atts    Raw attribute array.
	 * @param string $key     Attribute key.
	 * @param mixed  $default Fallback value.
	 * @return mixed
	 */
	private function get_atts_value( array $atts, $key, $default ) {
		return array_key_exists( $key, $atts ) ? $atts[ $key ] : $default;
	}

	/**
	 * Check whether the current request is a shortcode form submission.
	 *
	 * @return bool
	 */
	private function is_frontend_login_submission_request() {
		$request_method = isset( $_SERVER['REQUEST_METHOD'] ) ? strtoupper( sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) ) : 'GET';
		if ( 'POST' !== $request_method ) {
			return false;
		}

		// phpcs:disable WordPress.Security.NonceVerification.Missing
		$submitted_action = isset( $_POST[ self::SUBMIT_ACTION ] ) ? sanitize_text_field( wp_unslash( $_POST[ self::SUBMIT_ACTION ] ) ) : '';
		// phpcs:enable WordPress.Security.NonceVerification.Missing

		return '1' === $submitted_action;
	}

	/**
	 * Normalize the visual style attribute.
	 *
	 * @param string $style Raw style value.
	 * @return string
	 */
	private function normalize_style( $style ) {
		$style = is_string( $style ) ? sanitize_key( $style ) : 'default';

		if ( ! in_array( $style, array( 'default', 'minimal', 'compact' ), true ) ) {
			return 'default';
		}

		return $style;
	}

	/**
	 * Normalize the optional custom width for the login form.
	 *
	 * @param mixed $value Raw width value.
	 * @return int|string
	 */
	private function normalize_max_width( $value ) {
		if ( null === $value ) {
			return '';
		}

		$value = is_string( $value ) ? trim( $value ) : $value;

		if ( '' === $value || false === $value ) {
			return '';
		}

		$width = absint( $value );
		if ( 0 === $width ) {
			return '';
		}

		if ( $width < self::MAX_WIDTH_MIN ) {
			return self::MAX_WIDTH_MIN;
		}

		if ( $width > self::MAX_WIDTH_MAX ) {
			return self::MAX_WIDTH_MAX;
		}

		return $width;
	}

	/**
	 * Normalize redirect strings for shortcode and block attributes.
	 *
	 * @param mixed $value Raw redirect value.
	 * @return string
	 */
	private function normalize_redirect_to( $value ) {
		return is_string( $value ) ? trim( $value ) : '';
	}

	/**
	 * Convert shortcode-ish booleans to real booleans.
	 *
	 * @param mixed $value Raw value.
	 * @return bool
	 */
	private function to_bool( $value ) {
		if ( is_bool( $value ) ) {
			return $value;
		}

		$normalized = strtolower( trim( (string) $value ) );

		return ! in_array( $normalized, array( '', '0', 'false', 'no', 'off' ), true );
	}

	/**
	 * Resolve the redirect target for the current shortcode instance.
	 *
	 * @param array $atts Normalized shortcode attributes.
	 * @return string
	 */
	private function resolve_redirect_to( array $atts ) {
		$fallback = $this->get_error_return_url();

		// phpcs:disable WordPress.Security.NonceVerification.Recommended
		$query_redirect = isset( $_GET['redirect_to'] ) ? esc_url_raw( wp_unslash( $_GET['redirect_to'] ) ) : '';
		// phpcs:enable WordPress.Security.NonceVerification.Recommended
		$raw_redirect = $atts['redirect_to'];

		if ( '' === $raw_redirect && '' !== $query_redirect ) {
			$raw_redirect = $query_redirect;
		}

		$validated = wp_validate_redirect( $raw_redirect, $fallback );

		return ( is_string( $validated ) && '' !== $validated ) ? $validated : $fallback;
	}

	/**
	 * Return the redirect target from the submitted POST body.
	 *
	 * @return string
	 */
	private function get_redirect_to_from_post() {
		$fallback = $this->get_current_request_url();

		// phpcs:disable WordPress.Security.NonceVerification.Missing
		$raw_redirect = isset( $_POST['redirect_to'] ) ? esc_url_raw( wp_unslash( $_POST['redirect_to'] ) ) : '';
		// phpcs:enable WordPress.Security.NonceVerification.Missing

		$validated = wp_validate_redirect( $raw_redirect, $fallback );

		return ( is_string( $validated ) && '' !== $validated ) ? $validated : $fallback;
	}

	/**
	 * Check whether the password login came after a passkey setup attempt.
	 *
	 * @return bool
	 */
	private function has_passkey_setup_intent_from_post() {
		$field_name = class_exists( 'Guard_Dog_Passkeys' ) ? Guard_Dog_Passkeys::SETUP_INTENT_FIELD : 'guard_dog_passkey_setup_intent';

		// phpcs:disable WordPress.Security.NonceVerification.Missing
		$intent = isset( $_POST[ $field_name ] ) ? sanitize_text_field( wp_unslash( $_POST[ $field_name ] ) ) : '';
		// phpcs:enable WordPress.Security.NonceVerification.Missing

		return '1' === $intent;
	}

	/**
	 * Build the current request URL as an absolute URL.
	 *
	 * @return string
	 */
	private function get_current_request_url( array $remove_args = array() ) {
		$scheme = is_ssl() ? 'https' : 'http';
		$host   = isset( $_SERVER['HTTP_HOST'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_HOST'] ) ) : (string) wp_parse_url( home_url(), PHP_URL_HOST );

		if ( '' === $host ) {
			return home_url( '/' );
		}

		$request_uri = isset( $_SERVER['REQUEST_URI'] ) ? (string) wp_unslash( $_SERVER['REQUEST_URI'] ) : '/';
		if ( '' === $request_uri ) {
			$request_uri = '/';
		}

		$current_url = esc_url_raw( $scheme . '://' . $host . $request_uri );
		if ( ! empty( $remove_args ) && $current_url ) {
			$current_url = remove_query_arg( $remove_args, $current_url );
		}

		return $current_url ?: home_url( '/' );
	}

	/**
	 * Reset request-scoped submission state.
	 */
	private function reset_submission_state() {
		$this->submission_error = null;
		$this->submitted_login  = '';
	}

	/**
	 * Get the form action URL.
	 *
	 * @return string
	 */
	private function get_form_action_url() {
		return $this->get_error_return_url();
	}

	/**
	 * Return any captured submission errors as a flat array of messages.
	 *
	 * @return string[]
	 */
	public function get_submission_error_messages() {
		if ( ! is_wp_error( $this->submission_error ) ) {
			return array();
		}

		return array_values(
			array_filter(
				array_map(
					'strval',
					$this->submission_error->get_error_messages()
				)
			)
		);
	}

	/**
	 * Decide whether to show a continue link for logged-in users.
	 *
	 * @param string $redirect_to Resolved redirect URL.
	 * @return string
	 */
	private function get_logged_in_continue_url( $redirect_to ) {
		$current_url = $this->get_current_request_url();

		if ( '' === $redirect_to ) {
			return '';
		}

		if ( trailingslashit( $redirect_to ) === trailingslashit( $current_url ) ) {
			return '';
		}

		return $redirect_to;
	}

	/**
	 * Build an optional inline style string for the wrapper element.
	 *
	 * @param array $atts Normalized shortcode attributes.
	 * @return string
	 */
	private function get_wrapper_inline_style( array $atts ) {
		if ( empty( $atts['max_width'] ) ) {
			return '';
		}

		return sprintf( '--guard-dog-login-max-width: %dpx;', (int) $atts['max_width'] );
	}

	/**
	 * Enqueue shortcode assets once per request.
	 */
	private function enqueue_assets( array $atts, $is_logged_in = false ) {
		wp_enqueue_style(
			'guard-dog-login-form',
			GUARD_DOG_URL . 'assets/css/guard-dog-login-form.css',
			array(),
			GUARD_DOG_VERSION
		);

		if ( $is_logged_in ) {
			return;
		}

		wp_enqueue_script(
			'guard-dog-login-form',
			GUARD_DOG_URL . 'assets/js/guard-dog-login-form.js',
			array(),
			GUARD_DOG_VERSION,
			true
		);

		$this->enqueue_captcha_assets();
		$this->enqueue_passkey_assets( $atts );
		$this->enqueue_social_assets( $atts );
	}

	/**
	 * Force-load CAPTCHA assets when the frontend login shortcode is used.
	 */
	private function enqueue_captcha_assets() {
		if ( ! function_exists( 'Guard_Dog' ) || ! isset( Guard_Dog()->features['captcha'] ) ) {
			return;
		}

		$captcha = Guard_Dog()->features['captcha'];
		if ( ! is_object( $captcha ) ) {
			return;
		}

		if ( method_exists( $captcha, 'enqueue_captcha_scripts' ) ) {
			$captcha->enqueue_captcha_scripts();
		}

		if ( method_exists( $captcha, 'enqueue_captcha_styles' ) ) {
			$captcha->enqueue_captcha_styles();
		}
	}

	/**
	 * Force-load passkey assets when needed by the shortcode.
	 *
	 * @param array $atts Shortcode attributes.
	 */
	private function enqueue_passkey_assets( array $atts ) {
		if ( ! $atts['show_passkey'] || ! function_exists( 'Guard_Dog' ) || ! isset( Guard_Dog()->features['passkeys'] ) ) {
			return;
		}

		$passkeys = Guard_Dog()->features['passkeys'];
		if ( ! is_object( $passkeys ) || ! method_exists( $passkeys, 'enqueue_login_assets' ) ) {
			return;
		}

		$passkeys->enqueue_login_assets();
	}

	/**
	 * Force-load social login assets when needed by the shortcode.
	 *
	 * @param array $atts Shortcode attributes.
	 */
	private function enqueue_social_assets( array $atts ) {
		if ( ! $atts['show_social_login'] || ! function_exists( 'Guard_Dog' ) || ! isset( Guard_Dog()->features['social_login'] ) ) {
			return;
		}

		$social_login = Guard_Dog()->features['social_login'];
		if ( ! is_object( $social_login ) || ! method_exists( $social_login, 'enqueue_public_login_assets' ) ) {
			return;
		}

		$social_login->enqueue_public_login_assets();
	}

	/**
	 * Render CAPTCHA markup into a string for the shortcode template.
	 *
	 * @return string
	 */
	private function get_captcha_markup() {
		if ( ! function_exists( 'Guard_Dog' ) || ! isset( Guard_Dog()->features['captcha'] ) ) {
			return '';
		}

		$captcha = Guard_Dog()->features['captcha'];
		if ( ! is_object( $captcha ) || ! method_exists( $captcha, 'render_captcha' ) ) {
			return '';
		}

		ob_start();
		$captcha->render_captcha();

		return (string) ob_get_clean();
	}

	/**
	 * Get reusable passkey login markup for the shortcode.
	 *
	 * @param array $atts Shortcode attributes.
	 * @return string
	 */
	private function get_passkey_markup( array $atts ) {
		if ( ! $atts['show_passkey'] || ! function_exists( 'Guard_Dog' ) || ! isset( Guard_Dog()->features['passkeys'] ) ) {
			return '';
		}

		$passkeys = Guard_Dog()->features['passkeys'];
		if ( ! is_object( $passkeys ) || ! method_exists( $passkeys, 'get_login_button_markup' ) ) {
			return '';
		}

		return (string) $passkeys->get_login_button_markup();
	}

	/**
	 * Get reusable social login markup for the shortcode.
	 *
	 * @param array  $atts Shortcode attributes.
	 * @param string $redirect_to Successful-login redirect target.
	 * @param string $error_redirect_to OAuth-error redirect target.
	 * @return string
	 */
	private function get_social_login_markup( array $atts, $redirect_to, $error_redirect_to ) {
		if ( ! $atts['show_social_login'] || ! function_exists( 'Guard_Dog' ) || ! isset( Guard_Dog()->features['social_login'] ) ) {
			return '';
		}

		$social_login = Guard_Dog()->features['social_login'];
		if ( ! is_object( $social_login ) || ! method_exists( $social_login, 'get_login_buttons_markup' ) ) {
			return '';
		}

		return (string) $social_login->get_login_buttons_markup( $redirect_to, $error_redirect_to );
	}

	/**
	 * Get a clean return URL for frontend auth errors.
	 *
	 * @return string
	 */
	private function get_error_return_url() {
		return $this->get_current_request_url( array( 'social_login_error' ) );
	}

	/**
	 * Redirect and terminate execution.
	 *
	 * @param string $url Redirect target.
	 */
	private function redirect_to( $url ) {
		wp_safe_redirect( $url );
		exit;
	}
}
