<?php

declare(strict_types=1);

namespace SiteFort\Modules\Hardening\Tweaks;

use SiteFort\Modules\Hardening\Hardening;

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

/**
 * Login interstitial for forced password changes.
 *
 * Intercepts the WordPress authentication flow to require a password
 * change when the user's password is expired, flagged by an admin,
 * no longer meets policy, or is required after a role upgrade.
 *
 * Modeled on TwoFactor\LoginInterstitial.
 *
 */
class PasswordChangeInterstitial {

	/**
	 * Module settings.
	 *
	 * @var array
	 */
	private array $settings;

	/**
	 * Shared WordPress-level hardening policy evaluator.
	 *
	 * Reusing the live WordPressTweaks instance keeps the forced-change
	 * interstitial aligned with the same score thresholds and penalty strings
	 * used during login-time enforcement and registration validation.
	 */
	private WordPressTweaks $wp_tweaks;

	public function __construct( array $settings, WordPressTweaks $wp_tweaks ) {
		$this->settings = $settings;
		$this->wp_tweaks = $wp_tweaks;
	}

	private static function read_input_scalar( int $type, string $key ): string {
		$value = filter_input(
			$type,
			$key,
			FILTER_CALLBACK,
			[
				'options' => [ self::class, 'sanitize_interstitial_request_scalar' ],
				'flags'   => FILTER_REQUIRE_SCALAR,
			]
		);

		return is_string( $value ) ? $value : '';
	}

	private static function read_password_field( string $key ): string {
		$value = filter_input(
			INPUT_POST,
			$key,
			FILTER_CALLBACK,
			[
				'options' => [ self::class, 'normalize_password_input' ],
				'flags'   => FILTER_REQUIRE_SCALAR,
			]
		);

		return is_string( $value ) ? $value : '';
	}

	/**
	 * Preserve URL/token characters while removing invalid scalar request bytes.
	 *
	 * @param mixed $value
	 */
	public static function sanitize_interstitial_request_scalar( $value ): string {
		if ( ! is_scalar( $value ) ) {
			return '';
		}

		$value = str_replace( "\0", '', (string) $value );
		$value = preg_replace( '/[\x00-\x1F\x7F]/', '', $value );

		return is_string( $value ) ? trim( $value ) : '';
	}

	/**
	 * Passwords must not be text-sanitized before policy validation.
	 *
	 * @param mixed $value
	 */
	public static function normalize_password_input( $value ): string {
		return is_scalar( $value ) ? (string) $value : '';
	}

	/**
	 * Filter callback for 'authenticate' at priority 29 (before 2FA at 30).
	 *
	 * If the user needs a password change, prevent login and redirect
	 * to the password change interstitial form.
	 *
	 * @param \WP_User|\WP_Error|null $user     The authenticated user or error.
	 * @param string                  $username The username.
	 * @param string                  $password The password.
	 * @return \WP_User|\WP_Error|null
	 */
	public function intercept_authentication( $user, $username, $password ) {
		if ( ! ( $user instanceof \WP_User ) ) {
			return $user;
		}

		// API requests are fail-closed earlier in the auth flow because the
		// interactive interstitial cannot run there safely.
		if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
			return $user;
		}
		if ( defined( 'XMLRPC_REQUEST' ) && XMLRPC_REQUEST ) {
			return $user;
		}

		$reason = self::resolve_required_change_reason( $user, $this->settings );

		if ( ! $reason ) {
			return $user;
		}

		// Generate token and store login state in transient.
		$token = bin2hex( random_bytes( 32 ) );

		$redirect_to_raw = self::read_input_scalar( INPUT_GET, 'redirect_to' );
		if ( $redirect_to_raw === '' ) {
			$redirect_to_raw = self::read_input_scalar( INPUT_POST, 'redirect_to' );
		}
		$redirect_to = $redirect_to_raw !== ''
			? sanitize_url( $redirect_to_raw )
			: admin_url();

		$remember = self::read_input_scalar( INPUT_POST, 'rememberme' ) !== '';

		set_transient( 'sitefort_pwchange_' . $token, [
			'user_id'     => $user->ID,
			'redirect_to' => $redirect_to,
			'remember'    => $remember,
			'reason'      => $reason,
		], 600 ); // 10 minute expiry

		$login_url = add_query_arg( [
			'action'      => 'sitefort_password_change',
			'token'       => $token,
			'redirect_to' => rawurlencode( $redirect_to ),
		], wp_login_url() );

		wp_safe_redirect( $login_url );
		exit;
	}

	/**
	 * Render the password change form.
	 *
	 * Hooked to `login_form_sitefort_password_change`.
	 */
	public function render_form(): void {
		$request_method = strtoupper( self::read_input_scalar( INPUT_SERVER, 'REQUEST_METHOD' ) );
		if ( 'POST' === $request_method ) {
			$this->handle_submit();
			return;
		}

		$token = sanitize_text_field( self::read_input_scalar( INPUT_GET, 'token' ) );
		if ( empty( $token ) ) {
			wp_safe_redirect( wp_login_url() );
			exit;
		}

		$transient = get_transient( 'sitefort_pwchange_' . $token );
		if ( ! $transient ) {
			wp_safe_redirect( add_query_arg( 'sitefort_pw_error', 'expired', wp_login_url() ) );
			exit;
		}

		$redirect_to_raw = self::read_input_scalar( INPUT_GET, 'redirect_to' );
		$redirect_to = $redirect_to_raw !== ''
			? sanitize_url( $redirect_to_raw )
			: admin_url();
		$error       = sanitize_text_field( self::read_input_scalar( INPUT_GET, 'error' ) );
		$reason      = $transient['reason'] ?? 'expired';

		$this->render_form_page( $token, $redirect_to, $error, $reason );
		exit;
	}

	/**
	 * Process the password change form submission.
	 */
	private function handle_submit(): void {
		if ( ! wp_verify_nonce( sanitize_text_field( self::read_input_scalar( INPUT_POST, '_wpnonce' ) ), 'sitefort_password_change' ) ) {
			wp_safe_redirect( add_query_arg( 'sitefort_pw_error', 'nonce', wp_login_url() ) );
			exit;
		}

		$token = sanitize_text_field( self::read_input_scalar( INPUT_POST, 'sitefort_pw_token' ) );
		if ( empty( $token ) ) {
			wp_safe_redirect( wp_login_url() );
			exit;
		}

		$transient_key = 'sitefort_pwchange_' . $token;
		$transient     = get_transient( $transient_key );

		if ( ! $transient || ! isset( $transient['user_id'] ) ) {
			wp_safe_redirect( add_query_arg( 'sitefort_pw_error', 'expired', wp_login_url() ) );
			exit;
		}

		$user_id     = (int) $transient['user_id'];
		$redirect_to = $transient['redirect_to'] ?? admin_url();
		$remember    = $transient['remember'] ?? false;
		$reason      = $transient['reason'] ?? 'expired';

		$new_password     = self::read_password_field( 'pass1' );
		$confirm_password = self::read_password_field( 'pass2' );

		// Validate passwords match.
		if ( $new_password !== $confirm_password ) {
			$this->redirect_with_error( $token, $redirect_to, 'mismatch' );
		}

		if ( empty( $new_password ) ) {
			$this->redirect_with_error( $token, $redirect_to, 'empty' );
		}

		$user = get_userdata( $user_id );
		if ( ! $user ) {
			wp_safe_redirect( wp_login_url() );
			exit;
		}

			// Validate password strength through the shared WordPress hardening
			// policy, but only for users whose role is within the policy scope.
			if ( ! empty( $this->settings['enable_strong_passwords'] )
				&& WordPressTweaks::user_in_password_policy_scope( $this->settings, $user ) ) {
				if ( ! $this->wp_tweaks->is_password_strong_enough( $new_password, $user ) ) {
					$this->redirect_with_error( $token, $redirect_to, 'weak' );
				}
			}

		// Validate against HIBP if blocking or login-time check is enabled.
		if ( Hardening::is_breached_password_login_reset_enabled( $this->settings ) ) {
			$checker = new BreachedPasswordChecker( $this->settings );
			$result  = $checker->check_password( $new_password );
			if ( $result['breached'] ) {
				$this->redirect_with_error( $token, $redirect_to, 'breached' );
			}
		}

		// All checks passed - update the password.
		wp_set_password( $new_password, $user_id );

		\SiteFort\Modules\AuditLog\AuditLog::add_log(
			'Password Changed',
			[ 'username' => $user->user_login, 'reason' => $reason ],
			'warning',
			'user'
		);

		// Update tracking meta.
		update_user_meta( $user_id, 'sitefort_last_password_change', time() );
			delete_user_meta( $user_id, Hardening::PASSWORD_CHANGE_REQUIRED_META );
		Hardening::clear_password_policy_state( $user_id );

		// Delete transient.
		delete_transient( $transient_key );

		// Set auth cookie and redirect.
		wp_set_auth_cookie( $user_id, $remember );

		wp_safe_redirect( $redirect_to );
		exit;
	}

	/**
	 * Determine why the user needs to change their password.
	 *
	 * Shared by the browser interstitial and API authentication guard.
	 *
	 * @param \WP_User $user     The authenticated user.
	 * @param array    $settings Module settings.
	 * @return string|null Reason code, or null if no change required.
	 */
	public static function resolve_required_change_reason( \WP_User $user, array $settings ): ?string {
		// Only the "weak" reason is scoped to the strong-password roles; every
		// other reason (breach, admin-forced, expiration) applies to all users.
		$flagged = sanitize_key( (string) get_user_meta( $user->ID, Hardening::PASSWORD_CHANGE_REQUIRED_META, true ) );
		if ( $flagged !== '' && self::is_change_reason_enabled( $flagged, $settings ) ) {
			if ( $flagged !== 'weak' || WordPressTweaks::user_in_password_policy_scope( $settings, $user ) ) {
				return $flagged;
			}
		}

		// Password expiration applies to all users when enabled.
		$max_days = (int) ( $settings['password_expiration_days'] ?? 0 );
		if ( Hardening::is_password_expired( $user->ID, $max_days ) ) {
			return 'expired';
		}

		return null;
	}

	/**
	 * Determine whether a stored password-change reason is still backed by an active policy.
	 */
	private static function is_change_reason_enabled( string $reason, array $settings ): bool {
		switch ( $reason ) {
			case 'weak':
				return ! empty( $settings['enable_strong_passwords'] );
			case 'breached':
				return Hardening::is_breached_password_login_reset_enabled( $settings );
			case 'admin_forced':
				return true;
			default:
				return false;
		}
	}

	/**
	 * Redirect back to the form with an error code.
	 */
	private function redirect_with_error( string $token, string $redirect_to, string $error_code ): void {
		$url = add_query_arg( [
			'action'      => 'sitefort_password_change',
			'token'       => $token,
			'redirect_to' => rawurlencode( $redirect_to ),
			'error'       => $error_code,
		], wp_login_url() );

		wp_safe_redirect( $url );
		exit;
	}

	/**
	 * Enqueue password-change interstitial assets through WordPress.
	 */
	private function enqueue_form_assets(): void {
		$script_path = SITEFORT_PLUGIN_DIR . 'assets/password-change-interstitial.js';

		wp_enqueue_script( 'password-strength-meter' );
		wp_enqueue_script(
			'sitefort-password-change-interstitial',
			SITEFORT_PLUGIN_URL . 'assets/password-change-interstitial.js',
			[ 'password-strength-meter' ],
			file_exists( $script_path ) ? (string) filemtime( $script_path ) : SITEFORT_VERSION,
			true
		);
		wp_localize_script(
			'sitefort-password-change-interstitial',
			'sitefortPasswordChangeLabels',
			[
				'weak'      => __( 'Weak', 'sitefort' ),
				'medium'    => __( 'Medium', 'sitefort' ),
				'strong'    => __( 'Strong', 'sitefort' ),
				'veryWeak'  => __( 'Very weak', 'sitefort' ),
			]
		);
	}

	/**
	 * Render the password change form page.
	 */
	private function render_form_page( string $token, string $redirect_to, string $error, string $reason ): void {
		nocache_headers();

		$error_message = '';
		switch ( $error ) {
			case 'mismatch':
				$error_message = __( 'Passwords do not match. Please try again.', 'sitefort' );
				break;
			case 'empty':
				$error_message = __( 'Please enter a new password.', 'sitefort' );
				break;
			case 'weak':
				$error_message = __( 'Password is not strong enough. Please choose a stronger password.', 'sitefort' );
				break;
			case 'breached':
				$error_message = __( 'This password has appeared in data breaches. Please choose a different password.', 'sitefort' );
				break;
			case 'expired':
				$error_message = __( 'Your session has expired. Please log in again.', 'sitefort' );
				break;
		}

			$reason_message = $this->get_reason_message( $reason );

			login_header(
				__( 'Password Change Required', 'sitefort' ),
				$error_message
					? '<div id="login_error" class="notice notice-error"><p>' . esc_html( $error_message ) . '</p></div>'
					: '<p class="message">' . esc_html( $reason_message ) . '</p>',
				null
			);

			$this->enqueue_form_assets();

		$action_url = wp_login_url();
		?>
		<form name="sitefort_password_change_form" id="sitefort_password_change_form" method="post"
			  action="<?php echo esc_url( add_query_arg( 'action', 'sitefort_password_change', $action_url ) ); ?>">

			<div class="user-pass1-wrap">
				<p>
					<label for="pass1"><?php esc_html_e( 'New Password', 'sitefort' ); ?></label>
				</p>
				<div class="wp-pwd">
					<input type="password" name="pass1" id="pass1" class="input password-input"
						   size="24" value="" autocomplete="new-password" autofocus="autofocus"
						   data-pw="<?php echo esc_attr( wp_generate_password( 24 ) ); ?>" />
				</div>
			</div>

			<div class="user-pass2-wrap">
				<p>
					<label for="pass2"><?php esc_html_e( 'Confirm New Password', 'sitefort' ); ?></label>
				</p>
				<input type="password" name="pass2" id="pass2" class="input"
					   size="24" value="" autocomplete="new-password" />
			</div>

			<div id="pass-strength-result" class="hide-if-no-js" aria-live="polite" style="margin: 12px 0;"></div>

			<p class="description" style="margin-bottom: 16px;">
				<?php esc_html_e( 'Hint: Use a mix of upper and lower case letters, numbers, and symbols for the strongest password.', 'sitefort' ); ?>
			</p>

			<?php wp_nonce_field( 'sitefort_password_change' ); ?>
			<input type="hidden" name="sitefort_pw_token" value="<?php echo esc_attr( $token ); ?>" />
			<input type="hidden" name="redirect_to" value="<?php echo esc_attr( $redirect_to ); ?>" />

			<p class="submit">
				<input type="submit" name="wp-submit" id="wp-submit"
					   class="button button-primary button-large"
					   value="<?php esc_attr_e( 'Change Password', 'sitefort' ); ?>" />
			</p>
		</form>

		<?php
		login_footer();
	}

	/**
	 * Resolve the translated user-facing reason message.
	 *
	 * @param string $reason Reason code stored in the password change transient.
	 * @return string
	 */
	private function get_reason_message( string $reason ): string {
		switch ( $reason ) {
			case 'admin_forced':
				return __( 'An administrator has required you to change your password.', 'sitefort' );
			case 'weak':
				return __( 'Your current password no longer meets this site\'s password policy. Please choose a stronger password to continue.', 'sitefort' );
			case 'breached':
				return __( 'Your current password was found in a data breach. Please choose a new password.', 'sitefort' );
			case 'expired':
			default:
				return __( 'Your password has expired. Please choose a new password.', 'sitefort' );
		}
	}
}
