<?php
/**
 * Block Usernames Module
 *
 * Blocks commonly attacked usernames from being registered to prevent
 * spam bots and reduce brute force attack targets.
 *
 * @package BlockUsernames
 * @since 1.0.0
 */

namespace BlockUsernames;

use WP_PowerSuite\Core\AbstractModule;
use WP_PowerSuite\Core\OptionStoreModuleSettingsRepository;

/**
 * Block Usernames Module Class
 *
 * @since 1.0.0
 */
class Module extends AbstractModule {

	private const MODULE_ID = 'block-usernames';
	private const MODULE_VERSION = '1.2.0';

	/**
	 * Maximum custom blocked usernames allowed in settings.
	 */
	public const MAX_BLOCKED_USERNAMES = 500;

	/**
	 * Default blocked usernames merged when the default blocklist is enabled.
	 *
	 * @var list<string>
	 */
	public const DEFAULT_BLOCKED_USERNAMES = [
		'admin',
		'administrator',
		'root',
		'test',
		'demo',
		'support',
		'webmaster',
	];

	/**
	 * Settings cache for the active module instance.
	 *
	 * @var array<string, mixed>|null
	 */
	private static ?array $settings_cache = null;

	/**
	 * Resolved blocklist cache for the active module instance.
	 *
	 * @var list<string>|null
	 */
	private static ?array $blocked_logins_cache = null;

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

	public function getName(): string {
		return __( 'Block Usernames', 'wp-powersuite' );
	}

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

	public function getDescription(): string {
		return __( 'Blocks risky default usernames during registration so bots have fewer easy targets.', 'wp-powersuite' );
	}

	public function getContext(): string {
		return 'both';
	}

	/**
	 * @return list<string>
	 */
	public static function getDefaultBlockedUsernames(): array {
		return self::DEFAULT_BLOCKED_USERNAMES;
	}

	protected function registerHooks(): void {
		add_filter( 'illegal_user_logins', [ $this, 'mergeIllegalUserLogins' ], 10, 1 );
		add_filter( 'wp_powersuite_module_settings_allowed_keys', [ $this, 'filterAllowedSettingsKeys' ], 10, 2 );
		add_filter( 'wp_powersuite_sanitize_module_settings', [ $this, 'filterSanitizeModuleSettings' ], 10, 3 );
		add_action( 'wp_powersuite_module_settings_saved', [ $this, 'onSettingsSaved' ], 10, 2 );
	}

	/**
	 * Merge this module's blocklist into core illegal logins.
	 *
	 * @param array<int, string> $illegal Logins already marked disallowed.
	 * @return array<int, string>
	 */
	public function mergeIllegalUserLogins( array $illegal ): array {
		$blocked = self::resolveBlockedLogins( $this->getSettings() );
		if ( $blocked === [] ) {
			return $illegal;
		}

		$seen = [];
		foreach ( $illegal as $login ) {
			if ( ! is_string( $login ) || $login === '' ) {
				continue;
			}
			$seen[ strtolower( $login ) ] = true;
		}

		foreach ( $blocked as $login ) {
			$lower = strtolower( $login );
			if ( isset( $seen[ $lower ] ) ) {
				continue;
			}
			$seen[ $lower ] = true;
			$illegal[]      = $login;
		}

		return $illegal;
	}

	/**
	 * Whether a login is disallowed by WordPress illegal-login rules (including this module).
	 */
	public static function isLoginBlocked( string $username ): bool {
		if ( ! OptionStoreModuleSettingsRepository::isModuleEnabled( self::MODULE_ID ) ) {
			return false;
		}

		$normalized = sanitize_user( trim( $username ), true );
		if ( $normalized === '' ) {
			return false;
		}

		/** @var array<int, string> $illegal */
		$illegal = apply_filters( 'illegal_user_logins', [] );
		if ( $illegal === [] ) {
			return false;
		}

		$needle = strtolower( $normalized );
		foreach ( $illegal as $login ) {
			if ( is_string( $login ) && strtolower( $login ) === $needle ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Sanitize module settings before persistence.
	 *
	 * @param array<string, mixed> $raw Raw settings.
	 * @return array{0: array<string, mixed>, 1: string|null} Normalized settings and optional error message.
	 */
	public static function sanitizeSettings( array $raw ): array {
		$use_defaults = ! empty( $raw['use_default_blocklist'] );
		$blocked      = self::parseBlockedUsernamesInput( $raw['blocked_usernames'] ?? [] );

		if ( count( $blocked ) > self::MAX_BLOCKED_USERNAMES ) {
			return [
				[],
				sprintf(
					/* translators: %d: maximum number of blocked usernames */
					__( 'Blocked usernames list cannot exceed %d entries.', 'wp-powersuite' ),
					self::MAX_BLOCKED_USERNAMES
				),
			];
		}

		return [
			[
				'use_default_blocklist' => $use_defaults,
				'blocked_usernames'     => $blocked,
			],
			null,
		];
	}

	/**
	 * @param array<string, mixed> $raw
	 * @return array{use_default_blocklist: bool, blocked_usernames: list<string>}
	 */
	public static function normalizeSettings( array $raw ): array {
		[ $settings ] = self::sanitizeSettings( $raw );

		return $settings !== []
			? $settings
			: [
				'use_default_blocklist' => ! empty( $raw['use_default_blocklist'] ),
				'blocked_usernames'     => [],
			];
	}

	/**
	 * @param mixed $raw Custom blocked usernames from settings storage.
	 * @return list<string>
	 */
	public static function parseBlockedUsernamesInput( mixed $raw ): array {
		$lines = [];
		if ( is_string( $raw ) ) {
			$lines = preg_split( '/\r\n|\r|\n/', $raw ) ?: [];
		} elseif ( is_array( $raw ) ) {
			$lines = $raw;
		}

		$blocked = [];
		$seen    = [];
		foreach ( $lines as $line ) {
			if ( ! is_string( $line ) ) {
				continue;
			}
			$normalized = sanitize_user( trim( $line ), true );
			if ( $normalized === '' ) {
				continue;
			}
			$lower = strtolower( $normalized );
			if ( isset( $seen[ $lower ] ) ) {
				continue;
			}
			$seen[ $lower ] = true;
			$blocked[]      = $normalized;
		}

		return $blocked;
	}

	/**
	 * Build the effective blocked-login list from normalized settings.
	 *
	 * @param array<string, mixed> $settings Normalized module settings.
	 * @return list<string>
	 */
	public static function resolveBlockedLogins( array $settings ): array {
		$use_defaults = ! empty( $settings['use_default_blocklist'] );
		$blocked      = $use_defaults ? self::DEFAULT_BLOCKED_USERNAMES : [];

		$custom = $settings['blocked_usernames'] ?? [];
		if ( is_array( $custom ) ) {
			foreach ( $custom as $login ) {
				if ( is_string( $login ) && $login !== '' ) {
					$blocked[] = $login;
				}
			}
		}

		$unique = [];
		$seen   = [];
		foreach ( $blocked as $username ) {
			if ( ! is_string( $username ) || $username === '' ) {
				continue;
			}
			$lower = strtolower( $username );
			if ( ! isset( $seen[ $lower ] ) ) {
				$seen[ $lower ] = true;
				$unique[]       = $username;
			}
		}

		return $unique;
	}

	/**
	 * @param list<string>|null $allowed
	 * @return list<string>|null
	 */
	public function filterAllowedSettingsKeys( $allowed, string $module_id ): ?array {
		if ( $module_id !== self::MODULE_ID ) {
			return is_array( $allowed ) ? $allowed : null;
		}

		return [ 'use_default_blocklist', 'blocked_usernames' ];
	}

	/**
	 * @param array<string, mixed> $settings
	 * @return array<string, mixed>
	 */
	public function filterSanitizeModuleSettings( array $settings, string $module_id, string $context ): array {
		unset( $context );

		if ( $module_id !== self::MODULE_ID ) {
			return $settings;
		}

		return self::normalizeSettings( $settings );
	}

	/**
	 * @param array<string, mixed> $settings
	 */
	public function onSettingsSaved( string $module_id, array $settings ): void {
		unset( $settings );

		if ( $module_id === self::MODULE_ID ) {
			self::clearRuntimeCaches();
		}
	}

	public static function clearRuntimeCaches(): void {
		self::$settings_cache       = null;
		self::$blocked_logins_cache = null;
	}

	/**
	 * @return list<string>
	 */
	private function getBlockedUsernames(): array {
		if ( self::$blocked_logins_cache !== null ) {
			return self::$blocked_logins_cache;
		}

		self::$blocked_logins_cache = self::resolveBlockedLogins( $this->getSettings() );

		return self::$blocked_logins_cache;
	}

	/**
	 * @return array{use_default_blocklist: bool, blocked_usernames: list<string>}
	 */
	private function getSettings(): array {
		if ( self::$settings_cache !== null ) {
			return self::$settings_cache;
		}

		$settings = $this->moduleSettings()->all( [] );
		if ( ! is_array( $settings ) ) {
			$settings = [];
		}

		self::$settings_cache = self::normalizeSettings( $settings );

		return self::$settings_cache;
	}
}
