<?php

declare(strict_types=1);

namespace SecurWP\Modules\Hardening\Tweaks;

use SecurWP\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;
	}

	/**
	 * 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 = filter_input( INPUT_GET, 'redirect_to', FILTER_UNSAFE_RAW );
		if ( ! is_string( $redirect_to_raw ) || $redirect_to_raw === '' ) {
			$redirect_to_raw = filter_input( INPUT_POST, 'redirect_to', FILTER_UNSAFE_RAW );
		}
		$redirect_to = is_string( $redirect_to_raw ) && $redirect_to_raw !== ''
			? sanitize_url( wp_unslash( $redirect_to_raw ) )
			: admin_url();

		$remember_raw = filter_input( INPUT_POST, 'rememberme', FILTER_UNSAFE_RAW );
		$remember     = is_string( $remember_raw ) && $remember_raw !== '';

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

		$login_url = add_query_arg( [
			'action'      => 'securwp_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_securwp_password_change`.
	 */
	public function render_form(): void {
		$request_method = filter_input( INPUT_SERVER, 'REQUEST_METHOD', FILTER_UNSAFE_RAW );
		$request_method = is_string( $request_method ) ? strtoupper( wp_unslash( $request_method ) ) : '';
		if ( 'POST' === $request_method ) {
			$this->handle_submit();
			return;
		}

		$token = filter_input( INPUT_GET, 'token', FILTER_UNSAFE_RAW );
		$token = is_string( $token ) ? sanitize_text_field( wp_unslash( $token ) ) : '';
		if ( empty( $token ) ) {
			wp_safe_redirect( wp_login_url() );
			exit;
		}

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

		$redirect_to_raw = filter_input( INPUT_GET, 'redirect_to', FILTER_UNSAFE_RAW );
		$redirect_to = is_string( $redirect_to_raw )
			? sanitize_url( wp_unslash( $redirect_to_raw ) )
			: admin_url();
		$error_raw   = filter_input( INPUT_GET, 'error', FILTER_UNSAFE_RAW );
		$error       = is_string( $error_raw ) ? sanitize_text_field( wp_unslash( $error_raw ) ) : '';
		$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( wp_unslash( $_POST['_wpnonce'] ?? '' ) ), 'securwp_password_change' ) ) {
			wp_safe_redirect( add_query_arg( 'securwp_pw_error', 'nonce', wp_login_url() ) );
			exit;
		}

		$token = sanitize_text_field( wp_unslash( $_POST['securwp_pw_token'] ?? '' ) );
		if ( empty( $token ) ) {
			wp_safe_redirect( wp_login_url() );
			exit;
		}

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

		if ( ! $transient || ! isset( $transient['user_id'] ) ) {
			wp_safe_redirect( add_query_arg( 'securwp_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     = $_POST['pass1'] ?? ''; // phpcs:ignore
		$confirm_password = $_POST['pass2'] ?? ''; // phpcs:ignore

		// 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;
		}

		// Check password reuse if enabled.
		if ( ! empty( $this->settings['prevent_password_reuse'] ) ) {
			if ( wp_check_password( $new_password, $user->user_pass, $user->ID ) ) {
				$this->redirect_with_error( $token, $redirect_to, 'reuse' );
			}
		}

			// Validate password strength through the shared WordPress hardening policy.
			if ( ! empty( $this->settings['enable_strong_passwords'] ) ) {
				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 );

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

		// Update tracking meta.
		update_user_meta( $user_id, 'securwp_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 fail-closed authentication
	 * path so both flows enforce the same source-of-truth policy.
	 *
	 * @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 {
		// Check if explicitly flagged (role upgrade, admin forced, breached).
		$flagged = sanitize_key( (string) get_user_meta( $user->ID, Hardening::PASSWORD_CHANGE_REQUIRED_META, true ) );
		if ( $flagged !== '' && self::is_change_reason_enabled( $flagged, $settings ) ) {
			return $flagged;
		}

		// Check password expiration.
		$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 'role_upgrade':
				return ! empty( $settings['force_password_change_on_role_upgrade'] );
			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'      => 'securwp_password_change',
			'token'       => $token,
			'redirect_to' => rawurlencode( $redirect_to ),
			'error'       => $error_code,
		], wp_login_url() );

		wp_safe_redirect( $url );
		exit;
	}

	/**
	 * 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.', 'securwp' );
				break;
			case 'empty':
				$error_message = __( 'Please enter a new password.', 'securwp' );
				break;
			case 'reuse':
				$error_message = __( 'You cannot reuse your current password. Please choose a different one.', 'securwp' );
				break;
			case 'weak':
				$error_message = __( 'Password is not strong enough. Please choose a stronger password.', 'securwp' );
				break;
			case 'breached':
				$error_message = __( 'This password has appeared in data breaches. Please choose a different password.', 'securwp' );
				break;
			case 'expired':
				$error_message = __( 'Your session has expired. Please log in again.', 'securwp' );
				break;
		}

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

			login_header(
				__( 'Password Change Required', 'securwp' ),
				$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
			);

		// Enqueue password strength meter.
		wp_enqueue_script( 'password-strength-meter' );

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

			<div class="user-pass1-wrap">
				<p>
					<label for="pass1"><?php esc_html_e( 'New Password', 'securwp' ); ?></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', 'securwp' ); ?></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.', 'securwp' ); ?>
			</p>

			<?php wp_nonce_field( 'securwp_password_change' ); ?>
			<input type="hidden" name="securwp_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', 'securwp' ); ?>" />
			</p>
		</form>

		<script type="text/javascript">
			(function() {
				if ( typeof wp === 'undefined' || typeof wp.passwordStrength === 'undefined' ) return;
				var pass1 = document.getElementById('pass1');
				var pass2 = document.getElementById('pass2');
				var result = document.getElementById('pass-strength-result');
				function check() {
					var strength = wp.passwordStrength.meter( pass1.value, wp.passwordStrength.userInputDisallowedList(), pass2.value );
					result.className = 'hide-if-no-js';
					switch ( strength ) {
						case -1: result.textContent = ''; break;
						case 2:  result.textContent = '<?php echo esc_js( __( 'Weak', 'securwp' ) ); ?>'; result.className += ' bad'; break;
						case 3:  result.textContent = '<?php echo esc_js( __( 'Medium', 'securwp' ) ); ?>'; result.className += ' good'; break;
						case 4:  result.textContent = '<?php echo esc_js( __( 'Strong', 'securwp' ) ); ?>'; result.className += ' strong'; break;
						default: result.textContent = '<?php echo esc_js( __( 'Very weak', 'securwp' ) ); ?>'; result.className += ' short'; break;
					}
				}
				pass1.addEventListener('input', check);
				pass2.addEventListener('input', check);
			})();
		</script>

		<?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 'role_upgrade':
				return __( 'Your role has been upgraded. A stronger password is now required.', 'securwp' );
			case 'admin_forced':
				return __( 'An administrator has required you to change your password.', 'securwp' );
			case 'weak':
				return __( 'Your current password no longer meets this site\'s password policy. Please choose a stronger password to continue.', 'securwp' );
			case 'breached':
				return __( 'Your current password was found in a data breach. Please choose a new password.', 'securwp' );
			case 'expired':
			default:
				return __( 'Your password has expired. Please choose a new password.', 'securwp' );
		}
	}
}
