<?php

namespace CustomLoginErrorMessages;

use WP_PowerSuite\Auth\CredentialErrorNormalizer;
use WP_PowerSuite\Auth\LoginRequestDetector;
use WP_PowerSuite\Core\AbstractModule;

class Module extends AbstractModule {

	private const MODULE_ID = 'custom-login-error-messages';
	private const MODULE_VERSION = '1.3.0';
	private const MESSAGE_MAX_LEN = 500;

	/** @var array<string, mixed>|null */
	private ?array $settings = null;

	private static bool $login_filters_registered = false;

	public function getId(): string {
		return self::MODULE_ID;
	}

	public function getName(): string {
		return __( 'Generic Login Error Message', 'wp-powersuite' );
	}

	public function getVersion(): string {
		return self::MODULE_VERSION;
	}

	public function getDescription(): string {
		return __( 'Same friendly login error every time—stops people from fishing for valid usernames.', 'wp-powersuite' );
	}

	public function getContext(): string|array {
		return [ 'frontend', 'ajax', 'rest' ];
	}

	protected function registerHooks(): void {
		if ( LoginRequestDetector::shouldRegisterAuthHooksImmediately() ) {
			$this->registerLoginScreenFilters();
		}

		add_action( 'login_init', [ $this, 'registerLoginScreenFilters' ], 0 );
		add_action( 'wp_loaded', [ $this, 'maybeRegisterLoginScreenFilters' ], 0 );
		add_action( 'wp_ajax_nopriv_wpps_2fa_login', [ $this, 'registerAjaxLoginMessageFilter' ], 1 );
		add_action( 'woocommerce_init', [ $this, 'maybeAddWooCommerceLoginFilter' ], 0 );

		if ( defined( 'XMLRPC_REQUEST' ) && XMLRPC_REQUEST ) {
			add_action( 'init', [ $this, 'registerLoginScreenFilters' ], 0 );
		}
	}

	/**
	 * Register wp-login.php error filters (runs once per login screen request).
	 */
	public function registerLoginScreenFilters(): void {
		if ( self::$login_filters_registered ) {
			return;
		}
		self::$login_filters_registered = true;

		$this->registerCredentialAuthenticateFilter();

		add_filter( 'wp_login_errors', [ $this, 'filterCredentialErrors' ], PHP_INT_MAX - 50, 2 );
		add_filter( 'login_errors', [ $this, 'filterLoginErrorsString' ], 99998, 1 );
	}

	/**
	 * WooCommerce and other front-end login forms do not run login_init.
	 */
	public function maybeRegisterLoginScreenFilters(): void {
		if ( self::$login_filters_registered ) {
			return;
		}

		if ( LoginRequestDetector::shouldRegisterAuthHooksImmediately() ) {
			$this->registerLoginScreenFilters();
		}
	}

	/**
	 * Plain-text message for PowerSuite 2FA AJAX login (registered at priority 1 before ajaxLogin).
	 */
	public function registerAjaxLoginMessageFilter(): void {
		static $registered = false;
		if ( $registered ) {
			return;
		}
		$registered = true;
		add_filter( 'wp_powersuite_ajax_login_generic_message', [ $this, 'filterAjaxLoginGenericMessage' ], 10, 3 );
	}

	public function maybeAddWooCommerceLoginFilter(): void {
		if ( ! class_exists( '\WooCommerce' ) ) {
			return;
		}

		$this->registerCredentialAuthenticateFilter();
		add_filter( 'woocommerce_process_login_errors', [ $this, 'filterCredentialErrors' ], 999, 3 );
	}

	/**
	 * Locale-safe: normalize credential WP_Error during wp_authenticate (before display filters).
	 *
	 * @param \WP_User|\WP_Error|null $user     User, error, or null.
	 * @param string                  $username Login identifier.
	 * @param string                  $password Password.
	 * @return \WP_User|\WP_Error|null
	 */
	public function filterAuthenticateCredentialErrors( $user, $username, $password ) {
		unset( $username, $password );

		if ( ! $user instanceof \WP_Error ) {
			return $user;
		}

		return $this->anonymizeCredentialFailures( $user );
	}

	/**
	 * @param string          $default WordPress default text.
	 * @param \WP_Error|null  $error   Authentication error (for extensions).
	 * @param string          $context Source module identifier.
	 */
	public function filterAjaxLoginGenericMessage( $default, $error = null, $context = '' ) {
		unset( $default, $error, $context );
		return $this->getPlainGenericMessage();
	}

	public function filterCredentialErrors( $errors ) {
		if ( ! is_wp_error( $errors ) ) {
			return $errors;
		}

		return $this->anonymizeCredentialFailures( $errors );
	}

	/**
	 * Last-line sanitization of rendered login error HTML when wp_login_errors did not stick or was bypassed.
	 */
	public function filterLoginErrorsString( $html ): string {
		if ( ! is_string( $html ) ) {
			return '';
		}
		if ( $html === '' ) {
			return $html;
		}

		if ( str_contains( $html, 'wpps-lockout-message' ) ) {
			return $html;
		}

		if ( ! CredentialErrorNormalizer::loginErrorHtmlLooksLikeCredentialLeak( $html, true ) ) {
			return $html;
		}

		return $this->getFormattedGenericMessage();
	}

	private function registerCredentialAuthenticateFilter(): void {
		static $registered = false;
		if ( $registered ) {
			return;
		}
		$registered = true;
		add_filter( 'authenticate', [ $this, 'filterAuthenticateCredentialErrors' ], 99, 3 );
	}

	private function anonymizeCredentialFailures( \WP_Error $errors ): \WP_Error {
		if ( ! $errors->has_errors() ) {
			return $errors;
		}

		if ( ! CredentialErrorNormalizer::errorIsCredentialFailure( $errors, true ) ) {
			return $errors;
		}

		if ( ! CredentialErrorNormalizer::stripCredentialErrorCodes( $errors, true ) ) {
			return $errors;
		}

		$use_html = current_filter() !== 'woocommerce_process_login_errors';

		$errors->add(
			'invalid_username_or_incorrect_password',
			$use_html ? $this->getFormattedGenericMessage() : $this->getPlainGenericMessage()
		);

		return $errors;
	}

	private function getFormattedGenericMessage(): string {
		return '<strong>' . esc_html__( 'Error:', 'wp-powersuite' ) . '</strong> ' . esc_html( $this->getPlainGenericMessage() );
	}

	private function getPlainGenericMessage(): string {
		$settings = $this->getSettings();
		$raw      = isset( $settings['generic_error_message'] ) ? trim( (string) $settings['generic_error_message'] ) : '';

		if ( $raw === '' ) {
			$raw = __( 'Invalid username or password.', 'wp-powersuite' );
		}

		if ( function_exists( 'mb_substr' ) ) {
			return mb_substr( $raw, 0, self::MESSAGE_MAX_LEN );
		}

		return substr( $raw, 0, self::MESSAGE_MAX_LEN );
	}

	/**
	 * @return array<string, mixed>
	 */
	private function getSettings(): array {
		if ( $this->settings !== null ) {
			return $this->settings;
		}

		$this->settings = $this->moduleSettings()->all( $this->getDefaultSettings() );
		return $this->settings;
	}

	/**
	 * @return array<string, string>
	 */
	public function getDefaultSettings(): array {
		return [
			'generic_error_message' => 'Invalid username or password.',
		];
	}
}
