<?php
/**
 * Login Security
 *
 * Protects wp-login.php from:
 *  - Brute force (failed login counter + temporary IP block)
 *  - Username enumeration
 *  - Login URL disclosure (optional slug rename)
 *  - Logs every login attempt (success and failure)
 *
 * Architecture note:
 *  - 2FA: hooks are in place (gs_2fa_verify filter + gs_post_login_2fa action)
 *    so any 2FA add-on can plug in without modifying this file.
 *
 * @package GuardianShield
 */

namespace GuardianShield;

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

class LoginSecurity {

	private Logger $logger;
	private int    $max_attempts;
	private int    $lockout_minutes;

	public function __construct( Logger $logger ) {
		$this->logger          = $logger;
		$this->max_attempts    = (int) gs_get_option( 'max_login_attempts', 5 );
		$this->lockout_minutes = (int) gs_get_option( 'lockout_duration', 30 );
	}

	// ── Hook Registration ────────────────────────
	public function register_hooks(): void {
		// Track failed logins.
		add_action( 'wp_login_failed', [ $this, 'handle_failed_login' ], 10, 2 );

		// Track successful logins.
		add_action( 'wp_login', [ $this, 'handle_successful_login' ], 10, 2 );

		// Gate: run before authentication is completed.
		add_filter( 'authenticate', [ $this, 'check_ip_before_auth' ], 1, 3 );

		// Block username enumeration via ?author= queries.
		add_action( 'init', [ $this, 'block_author_enumeration' ], 2 );

		// Block username in REST API user endpoint.
		add_filter( 'rest_endpoints', [ $this, 'maybe_disable_user_rest' ] );

		// Remove login error hints (don't say "wrong password" vs "no user").
		add_filter( 'login_errors', [ $this, 'generic_login_error' ] );

		// Custom login URL (slug hide).
		if ( gs_get_option( 'hide_login_url', false ) ) {
			add_action( 'init',       [ $this, 'handle_custom_login_slug' ], 1 );
			add_filter( 'login_url',  [ $this, 'custom_login_url' ], 10, 3 );
		}

		// Logout on IP change (optional hardening).
		add_action( 'init', [ $this, 'check_session_ip' ] );

		// Rename 'admin' username on activation (if enabled).
		if ( gs_get_option( 'rename_admin_user', false ) ) {
			add_action( 'init', [ $this, 'maybe_rename_admin_username' ], 99 );
		}
	}

	// ── Pre-Auth Gate ────────────────────────────
	/**
	 * Runs before WP authenticates the user.
	 * If the IP is blocked, we short-circuit authentication entirely.
	 */
	public function check_ip_before_auth( $user, string $username, string $password ) {
		if ( empty( $username ) ) {
			return $user;
		}

		$ip = gs_get_client_ip();

		// Check transient counter.
		$attempts = (int) get_transient( "gs_login_fail_{$ip}" );

		if ( $attempts >= $this->max_attempts ) {
			// Log the blocked attempt.
			$this->logger->log_login_attempt( $ip, $username, 'blocked' );

			return new \WP_Error(
				'gs_ip_blocked',
				sprintf(
					/* translators: %d = lockout minutes */
					esc_html__( 'Too many failed login attempts. Please try again in %d minutes.', 'guardian-shield' ),
					$this->lockout_minutes
				)
			);
		}

		return $user;
	}

	// ── Failed Login Handler ─────────────────────
	public function handle_failed_login( string $username, \WP_Error $error ): void {
		$ip = gs_get_client_ip();

		$this->logger->log_login_attempt( $ip, $username, 'failure' );

		$transient_key = "gs_login_fail_{$ip}";
		$attempts      = (int) get_transient( $transient_key );
		$attempts++;

		// Set / extend the transient window.
		set_transient( $transient_key, $attempts, $this->lockout_minutes * MINUTE_IN_SECONDS );

		if ( $attempts >= $this->max_attempts ) {
			// Auto-add to temp_block in DB.
			/** @var Firewall $firewall */
			$firewall = \GuardianShield\Plugin::get_instance()->firewall;
			$firewall->temp_block_ip(
				$ip,
				$this->lockout_minutes,
				sprintf( 'Brute force: %d failed logins', $attempts )
			);

			// Fire alert hook (Mailer listens here).
			do_action( 'gs_brute_force_detected', $ip, $username, $attempts );
		}
	}

	// ── Successful Login Handler ─────────────────
	public function handle_successful_login( string $user_login, \WP_User $user ): void {
		$ip = gs_get_client_ip();

		$this->logger->log_login_attempt( $ip, $user_login, 'success' );

		// Clear failure counter on successful login.
		delete_transient( "gs_login_fail_{$ip}" );

		// Store session IP for change detection.
		update_user_meta( $user->ID, '_gs_session_ip', $ip );

		// Verwijder de login-toegang cookie nu de gebruiker is ingelogd.
		$cookie_token = sanitize_text_field( $_COOKIE['gs_login_access'] ?? '' );
		if ( $cookie_token ) {
			delete_transient( 'gs_login_token_' . $cookie_token );
			setcookie( 'gs_login_access', '', time() - 3600, COOKIEPATH, COOKIE_DOMAIN, is_ssl(), true );
		}

		// Fire 2FA post-login hook — 2FA add-ons listen here.
		do_action( 'gs_post_login_2fa', $user );
	}

	// ── Login Error Normalisation ────────────────
	/**
	 * Return a generic error so attackers can't distinguish
	 * "no such user" from "wrong password" — prevents user enumeration.
	 */
	public function generic_login_error(): string {
		return esc_html__( 'Invalid credentials. Please try again.', 'guardian-shield' );
	}

	// ── Author Enumeration Block ─────────────────
	public function block_author_enumeration(): void {
		if ( ! is_admin() && isset( $_GET['author'] ) ) {
			wp_die(
				esc_html__( 'Author enumeration is disabled.', 'guardian-shield' ),
				esc_html__( '403 Forbidden', 'guardian-shield' ),
				[ 'response' => 403 ]
			);
		}
	}

	// ── REST API User Endpoint ───────────────────
	public function maybe_disable_user_rest( array $endpoints ): array {
		if ( ! is_user_logged_in() ) {
			// Remove /wp/v2/users to prevent username enumeration.
			unset( $endpoints['/wp/v2/users'] );
			unset( $endpoints['/wp/v2/users/(?P<id>[\d]+)'] );
		}
		return $endpoints;
	}

	// ── Session IP Guard ─────────────────────────
	/**
	 * If the session IP changes mid-session (possible session hijack),
	 * log the event. Optionally destroy the session (configurable).
	 */
	public function check_session_ip(): void {
		if ( ! is_user_logged_in() ) {
			return;
		}

		$user_id     = get_current_user_id();
		$current_ip  = gs_get_client_ip();
		$stored_ip   = get_user_meta( $user_id, '_gs_session_ip', true );

		if ( $stored_ip && $stored_ip !== $current_ip ) {
			$this->logger->log_attack( [
				'ip_address'  => $current_ip,
				'attack_type' => 'session_ip_change',
				'severity'    => 2,
				'description' => sprintf( 'Session IP changed from %s to %s for user ID %d', $stored_ip, $current_ip, $user_id ),
				'blocked'     => 0,
			] );

			if ( gs_get_option( 'destroy_on_ip_change', false ) ) {
				wp_logout();
				wp_safe_redirect( wp_login_url() );
				exit;
			}
		}
	}

	// ── Login URL Hiding ─────────────────────────

	/**
	 * Onderschept de custom slug en stuurt door naar wp-login.php.
	 * Blokkeert directe toegang tot wp-login.php met 404.
	 */
	public function handle_custom_login_slug(): void {
		$slug = sanitize_title( gs_get_option( 'custom_login_slug', 'secure-login' ) );

		// Bepaal het huidige pad, zonder subdirectory prefix.
		$request_path = trim( (string) parse_url( $_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH ), '/' );
		$home_path    = trim( (string) parse_url( home_url(), PHP_URL_PATH ), '/' );
		if ( $home_path !== '' && str_starts_with( $request_path, $home_path ) ) {
			$request_path = trim( substr( $request_path, strlen( $home_path ) ), '/' );
		}

		// Custom slug bezocht → 302 naar echte wp-login.php met token in cookie.
		if ( $request_path === $slug ) {
			$token = bin2hex( random_bytes( 16 ) );
			setcookie( 'gs_login_access', $token, time() + 60, COOKIEPATH, COOKIE_DOMAIN, is_ssl(), true );
			set_transient( 'gs_login_token_' . $token, 1, 60 );

			$qs        = $_SERVER['QUERY_STRING'] ?? '';
			$login_url = site_url( 'wp-login.php' ) . ( $qs ? '?' . $qs : '' );
			header( 'Location: ' . $login_url, true, 302 );
			exit;
		}

		// Directe toegang tot wp-login.php → check cookie token.
		if ( str_contains( $request_path, 'wp-login' ) ) {
			$cookie_token = sanitize_text_field( $_COOKIE['gs_login_access'] ?? '' );
			if ( $cookie_token && get_transient( 'gs_login_token_' . $cookie_token ) ) {
				// Geldig cookie — verleng de geldigheid zodat alle wp-login.php
				// sub-requests (laden, submit, redirect) ook werken.
				set_transient( 'gs_login_token_' . $cookie_token, 1, 300 );
				setcookie( 'gs_login_access', $cookie_token, time() + 300, COOKIEPATH, COOKIE_DOMAIN, is_ssl(), true );
				return; // Sta wp-login.php toe normaal te laden.
			}
			// Geen geldig token → 404.
			status_header( 404 );
			nocache_headers();
			$template = locate_template( '404.php' );
			if ( $template ) {
				load_template( $template );
			} else {
				wp_die( '404 &mdash; Page not found.', '404 Not Found', [ 'response' => 404 ] );
			}
			exit;
		}
	}

	/** Filter login_url() zodat WordPress de custom slug gebruikt in links. */
	public function custom_login_url( string $login_url, string $redirect, bool $force_reauth ): string {
		$slug = sanitize_title( gs_get_option( 'custom_login_slug', 'secure-login' ) );
		$url  = home_url( '/' . $slug . '/' );
		if ( $redirect ) {
			$url = add_query_arg( 'redirect_to', rawurlencode( $redirect ), $url );
		}
		if ( $force_reauth ) {
			$url = add_query_arg( 'reauth', '1', $url );
		}
		return $url;
	}

	// ── Public Helpers ───────────────────────────

	/** How many failed attempts has this IP made recently? */
	public function get_attempt_count( string $ip ): int {
		return (int) get_transient( "gs_login_fail_{$ip}" );
	}

	/** Manually clear lockout for an IP (from admin UI). */
	public function clear_lockout( string $ip ): void {
		delete_transient( "gs_login_fail_{$ip}" );
	}

	// ── Admin Username Rename ────────────────────
	/**
	 * Hernoemt de 'admin' gebruikersnaam naar een willekeurige string.
	 * Wordt eenmalig uitgevoerd en slaat een vlag op zodat het niet
	 * herhaaldelijk gebeurt. Stuurt een e-mail met de nieuwe naam.
	 */
	public function maybe_rename_admin_username(): void {
		// Voorkom dat dit meerdere keren wordt uitgevoerd.
		if ( get_option( 'gs_admin_renamed' ) ) {
			return;
		}

		$user = get_user_by( 'login', 'admin' );
		if ( ! $user ) {
			// Geen 'admin' account gevonden — niets te doen.
			update_option( 'gs_admin_renamed', true, false );
			return;
		}

		// Genereer een veilige willekeurige gebruikersnaam (bijv. "gs_a3f8k2").
		$new_username = 'gs_' . wp_generate_password( 8, false, false );

		// Zorg dat de naam uniek is.
		while ( username_exists( $new_username ) ) {
			$new_username = 'gs_' . wp_generate_password( 8, false, false );
		}

		// WordPress heeft geen ingebouwde functie om user_login te wijzigen;
		// we doen het direct via $wpdb.
		global $wpdb;
		$updated = $wpdb->update(
			$wpdb->users,
			[ 'user_login' => $new_username ],
			[ 'ID' => $user->ID ],
			[ '%s' ],
			[ '%d' ]
		);

		if ( false === $updated ) {
			// Database update mislukt — log en stop.
			$this->logger->log_attack( [
				'ip_address'  => gs_get_client_ip(),
				'attack_type' => 'username_rename_failed',
				'severity'    => 2,
				'description' => 'Guardian Shield kon de admin gebruikersnaam niet hernoemen.',
				'blocked'     => 0,
			] );
			return;
		}

		// Sla nieuwe naam op in wp_options zodat admin het altijd kan terugvinden.
		update_option( 'gs_renamed_admin_username', $new_username, false );
		// Markeer als gedaan.
		update_option( 'gs_admin_renamed', true, false );

		// Stuur e-mailnotificatie via de Mailer.
		do_action( 'gs_username_renamed', 'admin', $new_username );
	}
}
