<?php

namespace ObfuscateAuthorSlugs;

use WP_REST_Response;
use WP_User;
use WP_PowerSuite\Core\AbstractModule;
use WP_PowerSuite\Core\Contracts\DataLocationsInterface;

class Module extends AbstractModule implements DataLocationsInterface {

	private const MODULE_ID = 'obfuscate-author-slugs';
	private const MODULE_VERSION = '1.3.0';
	private const USER_META_KEY = 'wpps_obfuscated_slug';
	private const REWRITE_FLUSH_TRANSIENT = 'wpps_author_slugs_flush_rewrite';
	private const SLUG_CACHE_GROUP = 'wpps_author_slugs';
	private const BACKFILL_CRON_HOOK = 'wpps_author_slugs_backfill_batch';

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

	public function getName(): string {
		return __( 'Obfuscate Author Slugs', 'wp-powersuite' );
	}

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

	public function getDescription(): string {
		return __( 'Replace guessable author profile links with random IDs. Author archives stay public under obfuscated URLs.', 'wp-powersuite' );
	}

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

	public function getDataLocations(): array {
		return [
			'user_meta' => [ self::USER_META_KEY ],
		];
	}

	protected function registerHooks(): void {
		add_action( 'init', [ $this, 'addRewriteRules' ], 10 );
		add_action( 'init', [ $this, 'maybeFlushRewriteRules' ], 999 );
		add_action( 'init', [ $this, 'blockAuthorEnumeration' ], 1 );
		add_filter( 'author_link', [ $this, 'obfuscateAuthorLink' ], 10, 3 );
		add_action( 'user_register', [ $this, 'generateSlugForUser' ], 10, 1 );
		add_action( 'wp_powersuite_module_toggled', [ $this, 'onModuleToggled' ], 10, 2 );
		add_action( self::BACKFILL_CRON_HOOK, [ $this, 'runBackfillBatch' ] );

		if ( ! \WP_PowerSuite\Core\OptionStoreModuleSettingsRepository::isModuleEnabled( 'disable-rest-api' ) ) {
			add_action( 'rest_api_init', [ $this, 'registerRestUserGuard' ], 1 );
		}

		if ( ! is_admin() && ! $this->isRestRequest() ) {
			add_action( 'parse_query', [ $this, 'parseObfuscatedAuthorQuery' ], 10 );
			add_action( 'template_redirect', [ $this, 'blockRealUsernameAccess' ], 1 );
		}

		if ( is_admin() ) {
			add_action( 'admin_init', [ $this, 'maybeScheduleBackfill' ] );
		}

		add_action( 'rest_api_init', [ $this, 'registerRestFilters' ] );
	}

	public function onModuleToggled( string $module_id, bool $enable ): void {
		if ( self::MODULE_ID !== $module_id ) {
			return;
		}

		set_transient( self::REWRITE_FLUSH_TRANSIENT, 1, MINUTE_IN_SECONDS );

		if ( $enable ) {
			delete_transient( 'wpps_author_slugs_generated' );
			$this->scheduleBackfill();
			return;
		}

		delete_transient( 'wpps_author_slugs_rewrite_flushed' );
		delete_transient( 'wpps_author_slugs_generated' );
		delete_transient( 'wpps_author_slugs_batch_lock' );
		wp_clear_scheduled_hook( self::BACKFILL_CRON_HOOK );
		self::clearObfuscatedSlugs();
	}

	public function registerRestUserGuard(): void {
		static $registered = false;
		if ( $registered ) {
			return;
		}
		$registered = true;
		add_filter( 'rest_pre_dispatch', [ $this, 'blockAnonymousUsersRest' ], 10, 3 );
	}

	public function registerRestFilters(): void {
		add_filter( 'rest_prepare_user', [ $this, 'obfuscateRestUserSlug' ], 10, 3 );
	}

	public function addRewriteRules(): void {
		$base = preg_quote( $this->getAuthorBase(), '/' );

		add_rewrite_rule(
			'^' . $base . '/([a-f0-9]{10})/?$',
			'index.php?author_name=$matches[1]',
			'top'
		);
		add_rewrite_rule(
			'^' . $base . '/([a-f0-9]{10})/page/?([0-9]{1,})/?$',
			'index.php?author_name=$matches[1]&paged=$matches[2]',
			'top'
		);
		add_rewrite_rule(
			'^' . $base . '/([a-f0-9]{10})/feed/(feed|rdf|rss|rss2|atom)/?$',
			'index.php?author_name=$matches[1]&feed=$matches[2]',
			'top'
		);
	}

	public function maybeFlushRewriteRules(): void {
		if ( get_transient( self::REWRITE_FLUSH_TRANSIENT ) ) {
			delete_transient( self::REWRITE_FLUSH_TRANSIENT );
			flush_rewrite_rules( false );
			set_transient( 'wpps_author_slugs_rewrite_flushed', true, YEAR_IN_SECONDS );
			return;
		}

		if ( is_admin() && ! get_transient( 'wpps_author_slugs_rewrite_flushed' ) ) {
			flush_rewrite_rules( false );
			set_transient( 'wpps_author_slugs_rewrite_flushed', true, YEAR_IN_SECONDS );
		}
	}

	public function obfuscateAuthorLink( string $link, int $author_id, string $author_nicename ): string {
		unset( $author_nicename );
		$obfuscated_slug = $this->getObfuscatedSlug( $author_id );
		if ( ! $obfuscated_slug ) {
			return $link;
		}

		$base = $this->getAuthorBase();

		if ( get_option( 'permalink_structure' ) ) {
			$path = user_trailingslashit( $base . '/' . $obfuscated_slug );
			return home_url( $path );
		}

		return add_query_arg( 'author_name', $obfuscated_slug, home_url( '/' ) );
	}

	public function parseObfuscatedAuthorQuery( \WP_Query $query ): void {
		if ( ! $query->is_main_query() ) {
			return;
		}

		$author_name = $query->get( 'author_name' );
		if ( $author_name === '' || $author_name === null ) {
			return;
		}

		if ( ! $this->isObfuscatedSlug( (string) $author_name ) ) {
			$this->setQueryAuthor404( $query );
			return;
		}

		$query->set( 'author', '' );

		$user_id = $this->getUserIdByObfuscatedSlug( (string) $author_name );
		if ( $user_id ) {
			$query->set( 'author', (string) $user_id );
			$query->set( 'author_name', '' );
			return;
		}

		$this->setQueryAuthor404( $query );
	}

	public function blockAuthorEnumeration(): void {
		if ( is_admin() || ( defined( 'DOING_CRON' ) && DOING_CRON ) ) {
			return;
		}
		if ( $this->isRestRequest() ) {
			return;
		}
		if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) {
			return;
		}

		if ( ! isset( $_GET['author'] ) ) {
			return;
		}

		$author_param = sanitize_text_field( wp_unslash( (string) $_GET['author'] ) );
		if ( $author_param === '' || $author_param === '0' ) {
			return;
		}

		nocache_headers();
		wp_safe_redirect( home_url( '/' ), 301 );
		exit;
	}

	private function getObfuscatedSlug( int $user_id ): ?string {
		$slug = get_user_meta( $user_id, self::USER_META_KEY, true );
		if ( is_string( $slug ) && $slug !== '' && $this->isObfuscatedSlug( $slug ) ) {
			return $slug;
		}

		return $this->generateSlugForUser( $user_id );
	}

	public function generateSlugForUser( int $user_id ): ?string {
		$user = get_userdata( $user_id );
		if ( ! $user ) {
			return null;
		}

		$existing_slug = get_user_meta( $user_id, self::USER_META_KEY, true );
		if ( is_string( $existing_slug ) && $existing_slug !== '' && $this->isObfuscatedSlug( $existing_slug ) ) {
			return $existing_slug;
		}

		$attempts = 0;
		do {
			$slug     = bin2hex( random_bytes( 5 ) );
			$owner_id = $this->getUserIdByObfuscatedSlug( $slug );
			$attempts++;
		} while ( $owner_id !== null && $owner_id !== $user_id && $attempts < 10 );

		update_user_meta( $user_id, self::USER_META_KEY, $slug );
		$this->cacheSlugOwner( $slug, $user_id );

		return $slug;
	}

	private function getUserIdByObfuscatedSlug( string $slug ): ?int {
		if ( ! $this->isObfuscatedSlug( $slug ) ) {
			return null;
		}

		$cache_key = 'slug_' . $slug;
		$found     = false;
		$cached    = wp_cache_get( $cache_key, self::SLUG_CACHE_GROUP, false, $found );
		if ( $found ) {
			return $cached === 0 ? null : (int) $cached;
		}

		global $wpdb;

		$user_id = $wpdb->get_var(
			$wpdb->prepare(
				"SELECT user_id FROM {$wpdb->usermeta} WHERE meta_key = %s AND meta_value = %s LIMIT 1",
				self::USER_META_KEY,
				$slug
			)
		);

		$resolved = $user_id ? (int) $user_id : null;
		wp_cache_set( $cache_key, $resolved ?? 0, self::SLUG_CACHE_GROUP, HOUR_IN_SECONDS );

		return $resolved;
	}

	public function obfuscateRestUserSlug( $response, $user, $_request ) {
		unset( $_request );
		if ( ! $response instanceof WP_REST_Response || ! $user instanceof WP_User ) {
			return $response;
		}

		$data = $response->get_data();
		if ( ! isset( $data['slug'] ) ) {
			return $response;
		}

		$obfuscated = $this->getObfuscatedSlug( (int) $user->ID );
		if ( $obfuscated ) {
			$data['slug'] = $obfuscated;
			$response->set_data( $data );
		}

		return $response;
	}

	/**
	 * @param mixed $result
	 * @return mixed
	 */
	public function blockAnonymousUsersRest( $result, $server, $request ) {
		unset( $server );
		if ( ! empty( $result ) ) {
			return $result;
		}
		if ( ! $request instanceof \WP_REST_Request ) {
			return $result;
		}
		if ( is_user_logged_in() ) {
			return $result;
		}
		$route = (string) $request->get_route();
		if ( '' === $route || ! preg_match( '#^/wp/v2/users(?:/|$)#', $route ) ) {
			return $result;
		}
		return new \WP_Error(
			'rest_user_enumeration_forbidden',
			__( 'You are not allowed to access user data.', 'wp-powersuite' ),
			[ 'status' => rest_authorization_required_code() ]
		);
	}

	public function blockRealUsernameAccess(): void {
		if ( ! is_author() ) {
			return;
		}

		global $wp_query;
		$author_name = $wp_query->get( 'author_name' );
		if ( $author_name !== '' && $author_name !== null && ! $this->isObfuscatedSlug( (string) $author_name ) ) {
			$this->sendAuthor404();
		}
	}

	public function maybeScheduleBackfill(): void {
		if ( get_transient( 'wpps_author_slugs_generated' ) ) {
			return;
		}

		$this->scheduleBackfill();
	}

	public function runBackfillBatch(): void {
		if ( get_transient( 'wpps_author_slugs_batch_lock' ) ) {
			$this->scheduleBackfill();
			return;
		}
		set_transient( 'wpps_author_slugs_batch_lock', true, MINUTE_IN_SECONDS );

		global $wpdb;
		$users_without_slugs = $wpdb->get_col(
			$wpdb->prepare(
				"SELECT u.ID FROM {$wpdb->users} u LEFT JOIN {$wpdb->usermeta} um ON u.ID = um.user_id AND um.meta_key = %s WHERE um.user_id IS NULL LIMIT 50",
				self::USER_META_KEY
			)
		);

		if ( empty( $users_without_slugs ) ) {
			set_transient( 'wpps_author_slugs_generated', true, WEEK_IN_SECONDS );
			delete_transient( 'wpps_author_slugs_batch_lock' );
			return;
		}

		foreach ( $users_without_slugs as $user_id ) {
			$this->generateSlugForUser( (int) $user_id );
		}

		delete_transient( 'wpps_author_slugs_batch_lock' );
		$this->scheduleBackfill();
	}

	public static function clearObfuscatedSlugs(): void {
		global $wpdb;

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$wpdb->delete( $wpdb->usermeta, [ 'meta_key' => self::USER_META_KEY ] );
		if ( function_exists( 'wp_cache_flush_group' ) ) {
			wp_cache_flush_group( self::SLUG_CACHE_GROUP );
		}
	}

	private function scheduleBackfill(): void {
		if ( get_transient( 'wpps_author_slugs_generated' ) ) {
			return;
		}

		if ( wp_next_scheduled( self::BACKFILL_CRON_HOOK ) ) {
			return;
		}

		wp_schedule_single_event( time() + 1, self::BACKFILL_CRON_HOOK );
	}

	private function getAuthorBase(): string {
		global $wp_rewrite;

		$base = 'author';
		if ( $wp_rewrite instanceof \WP_Rewrite && is_string( $wp_rewrite->author_base ) && $wp_rewrite->author_base !== '' ) {
			$base = $wp_rewrite->author_base;
		}

		return sanitize_title( $base );
	}

	private function isObfuscatedSlug( string $slug ): bool {
		return (bool) preg_match( '/^[a-f0-9]{10}$/', $slug );
	}

	private function isRestRequest(): bool {
		return defined( 'REST_REQUEST' ) && REST_REQUEST;
	}

	private function setQueryAuthor404( \WP_Query $query ): void {
		$query->set_404();
		status_header( 404 );
		nocache_headers();
	}

	private function sendAuthor404(): void {
		global $wp_query;

		$wp_query->set_404();
		status_header( 404 );
		nocache_headers();
		exit;
	}

	private function cacheSlugOwner( string $slug, int $user_id ): void {
		wp_cache_set( 'slug_' . $slug, $user_id, self::SLUG_CACHE_GROUP, HOUR_IN_SECONDS );
	}

}
