<?php

declare(strict_types=1);

namespace SecurWP\Modules\TwoFactor;

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

use SecurWP\Core\Database\SchemaInspector;
use SecurWP\Includes\Interfaces\Module;
use SecurWP\Includes\Module_Loader;

/**
 * Two-Factor Authentication module controller.
 *
 * Orchestrates the method-aware two-factor authentication system including
 * login interception, REST API routes, profile management, and settings.
 *
 */
class TwoFactor implements Module {

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

	/**
	 * The REST API handler.
	 *
	 * @var TwoFactorApi
	 */
	private TwoFactorApi $api;

	/**
	 * The login interstitial handler.
	 *
	 * @var LoginInterstitial
	 */
	private LoginInterstitial $login_interstitial;

	/**
	 * Constructor.
	 *
	 * @param string $plugin_name The name of the plugin.
	 * @param string $version     The version of this plugin.
	 */
	public function __construct( string $plugin_name, string $version ) {
		$this->load_settings();
	}

	/**
	 * Initialize the module and register hooks via the loader.
	 *
	 * @param Module_Loader $loader The module loader instance.
	 */
	public function initialize( Module_Loader $loader ): void {
		// Always clean up 2FA data when a user is deleted (even if the module is disabled).
		$loader->add_action( 'delete_user', $this, 'on_delete_user' );

		// Keep the REST and pending-challenge login endpoints available even if
		// policy is disabled mid-session so existing challenge tokens resolve
		// cleanly instead of falling through to an unhandled login action.
		$this->api                = new TwoFactorApi();
		$this->login_interstitial = new LoginInterstitial( $this->settings );
		$loader->add_action( 'rest_api_init', $this->api, 'register_routes' );
		$loader->add_action( 'login_form_securwp_2fa', $this->login_interstitial, 'render_2fa_form' );

		// Only register interactive enforcement and profile management hooks when
		// the module is enabled. The REST controller still loads so settings can
		// be inspected and administrators can enable the feature.
		if ( empty( $this->settings['enabled'] ) ) {
			return;
		}

		// Register authenticate filter at priority 30 to intercept after
		// WordPress has validated the username/password (priority 20).
		$loader->add_filter( 'authenticate', $this->login_interstitial, 'intercept_authentication', 30, 3 );

		// Show 2FA status on user profile pages.
		$loader->add_action( 'show_user_profile', $this, 'render_profile_section' );
		$loader->add_action( 'edit_user_profile', $this, 'render_profile_section' );

		// Enqueue profile page JS.
		$loader->add_action( 'admin_enqueue_scripts', $this, 'enqueue_profile_script' );

		// Show admin notice for users whose role requires 2FA.
		$loader->add_action( 'admin_notices', $this, 'render_enforcement_notice' );

		// After grace period expires, force users to the profile page until 2FA is set up.
		$loader->add_action( 'admin_init', $this, 'enforce_2fa_setup' );
	}

	/**
	 * Remove 2FA data when a WordPress user is deleted.
	 *
	 * @param int $user_id The ID of the user being deleted.
	 */
		public function on_delete_user( int $user_id ): void {
			global $wpdb;
			$table = $wpdb->prefix . 'securwp_two_factor';

			RecoveryCodes::clear_codes( $user_id );
			LoginSessionStore::delete_for_user( $user_id );

			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Deletes a single row from a plugin-owned table during user cleanup.
			$wpdb->delete( $table, [ 'user_id' => $user_id ], [ '%d' ] );
		}

	/**
	 * After the grace period expires, redirect enforced users to their profile page
	 * until they set up 2FA. Allows profile.php, admin-ajax.php, and REST API requests
	 * so the inline setup flow works.
	 */
	public function enforce_2fa_setup(): void {
		$user = wp_get_current_user();
		if ( ! $user || ! $user->ID ) {
			return;
		}

		// Already has 2FA enabled; nothing to enforce.
		if ( LoginInterstitial::is_2fa_enabled_for_user( $user->ID ) ) {
			return;
		}

		$enforce_roles = $this->settings['enforce_for_roles'] ?? [];
		if ( empty( $enforce_roles ) ) {
			return;
		}

		// Check if user has any enforced role.
		$user_roles = (array) $user->roles;
		if ( empty( array_intersect( $user_roles, $enforce_roles ) ) ) {
			return;
		}

		$grace_days = (int) ( $this->settings['grace_period_days'] ?? 3 );
		$since      = LoginInterstitial::get_enforcement_start( $user->ID );
		$elapsed    = ( time() - $since ) / DAY_IN_SECONDS;

		// Still within grace period; the admin notice handles the reminder.
		if ( $elapsed < $grace_days ) {
			return;
		}

		// Grace period expired. Allow profile page, AJAX/REST (needed for setup),
		// and the SecurWP plugin page so admins can manage enforcement or set up 2FA.
		global $pagenow;
		$allowed_pages = [ 'profile.php', 'admin-ajax.php' ];
		if ( in_array( $pagenow, $allowed_pages, true ) ) {
			return;
		}
		$plugin_page = '';
		// phpcs:disable WordPress.Security.NonceVerification.Recommended -- Read-only admin routing check for the plugin page slug.
		if ( isset( $_GET['page'] ) ) {
			$plugin_page = sanitize_text_field( wp_unslash( $_GET['page'] ) );
		}
		// phpcs:enable WordPress.Security.NonceVerification.Recommended
		if ( $pagenow === 'admin.php' && str_starts_with( $plugin_page, 'securwp' ) ) {
			return;
		}

		wp_safe_redirect( admin_url( 'profile.php#securwp-2fa-profile' ) );
		exit;
	}

	/**
	 * Enqueue the profile page 2FA script on profile.php and user-edit.php.
	 *
	 * @param string $hook_suffix The current admin page hook.
	 */
	public function enqueue_profile_script( string $hook_suffix ): void {
		if ( $hook_suffix !== 'profile.php' && $hook_suffix !== 'user-edit.php' ) {
			return;
		}

		$path = SECURWP_PLUGIN_DIR . 'assets/profile-2fa.js';
		if ( ! file_exists( $path ) ) {
			return;
		}

		wp_enqueue_script(
			'securwp-profile-2fa',
			SECURWP_PLUGIN_URL . 'assets/profile-2fa.js',
			[],
			(string) filemtime( $path ),
			true
		);
	}

	/**
	 * Show an admin notice when the current user's role requires 2FA but they haven't set it up.
	 *
	 * During the grace period: dismissible warning with days remaining.
	 * After the grace period: non-dismissible error notice.
	 */
	public function render_enforcement_notice(): void {
		$user = wp_get_current_user();
		if ( ! $user || ! $user->ID ) {
			return;
		}

		// Check if user already has 2FA enabled.
		if ( LoginInterstitial::is_2fa_enabled_for_user( $user->ID ) ) {
			return;
		}

		$enforce_roles = $this->settings['enforce_for_roles'] ?? [];
		if ( empty( $enforce_roles ) ) {
			return;
		}

		// Check if user has any enforced role.
		$user_roles = (array) $user->roles;
		if ( empty( array_intersect( $user_roles, $enforce_roles ) ) ) {
			return;
		}

			$grace_days      = (int) ( $this->settings['grace_period_days'] ?? 3 );
			$since           = LoginInterstitial::get_enforcement_start( $user->ID );
			$elapsed         = ( time() - $since ) / DAY_IN_SECONDS;
			$remaining       = max( 0, (int) ceil( $grace_days - $elapsed ) );
			$remaining_count = absint( $remaining );
			$profile_url = get_edit_profile_url( $user->ID ) . '#securwp-2fa-profile';

		if ( $remaining > 0 ) :
			// Grace period active: dismissible warning with countdown.
			?>
			<div class="notice notice-warning is-dismissible">
				<p>
					<strong><?php esc_html_e( 'SecurWP:', 'securwp' ); ?></strong>
					<?php
						echo esc_html(
							sprintf(
								/* translators: %d: number of days remaining */
								_n(
									'Your account role requires two-factor authentication. You have %d day left to set it up.',
									'Your account role requires two-factor authentication. You have %d days left to set it up.',
									$remaining_count,
									'securwp'
								),
								$remaining_count
							)
						);
					?>
					<a href="<?php echo esc_url( $profile_url ); ?>"><?php esc_html_e( 'Set up 2FA now', 'securwp' ); ?></a>
				</p>
			</div>
		<?php else : ?>
			<div class="notice notice-error">
				<p>
					<strong><?php esc_html_e( 'SecurWP:', 'securwp' ); ?></strong>
					<?php esc_html_e( 'Two-factor authentication is required for your account. Please set it up now to continue using the site.', 'securwp' ); ?>
					<a href="<?php echo esc_url( $profile_url ); ?>"><?php esc_html_e( 'Set up 2FA', 'securwp' ); ?></a>
				</p>
			</div>
			<?php
		endif;
	}

	/**
	 * Render the 2FA section on the user profile page.
	 *
	 * For own profiles: shows inline setup, management, and disable controls.
	 * For admins viewing other users: shows read-only status.
	 *
	 * @param \WP_User $user The user whose profile is being displayed.
	 */
	public function render_profile_section( \WP_User $user ): void {
		$is_enabled = LoginInterstitial::is_2fa_enabled_for_user( $user->ID );
		$method     = LoginInterstitial::get_user_method( $user->ID );
		$is_own     = get_current_user_id() === $user->ID;

		$method_label = $method === 'email' ? __( 'Email Code', 'securwp' ) : __( 'Authenticator App', 'securwp' );

		$allowed = self::sanitize_allowed_methods( $this->settings['allowed_methods'] ?? self::get_default_settings()['allowed_methods'] );
		if ( empty( $allowed ) ) {
			$allowed = self::get_default_settings()['allowed_methods'];
		}
		$allow_totp  = in_array( 'totp', $allowed, true );
		$allow_email = in_array( 'email', $allowed, true );

		$remaining = 0;
		if ( $is_enabled ) {
			$remaining = \SecurWP\Modules\TwoFactor\RecoveryCodes::get_remaining_count( $user->ID );
		}
		?>
		<h2><?php esc_html_e( 'Two-Factor Authentication', 'securwp' ); ?></h2>
		<div id="securwp-2fa-profile"
			data-nonce="<?php echo esc_attr( wp_create_nonce( 'wp_rest' ) ); ?>"
			data-api-root="<?php echo esc_attr( rest_url() ); ?>"
			data-user-id="<?php echo esc_attr( (string) $user->ID ); ?>"
			data-is-own="<?php echo $is_own ? '1' : '0'; ?>">
		<table class="form-table" role="presentation">
			<tr id="sp2fa-status-row">
				<th><label><?php esc_html_e( 'Status', 'securwp' ); ?></label></th>
				<td>
					<?php if ( $is_enabled ) : ?>
						<span class="sp2fa-badge sp2fa-badge-on">
							<svg width="14" height="14" viewBox="0 0 16 16" fill="none"><path d="M4 8.5l3 3 5-5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
							<?php esc_html_e( 'Enabled', 'securwp' ); ?>
						</span>
						<span class="sp2fa-method-label"><?php echo esc_html( $method_label ); ?></span>
						<?php if ( $remaining > 0 ) : ?>
							<span class="sp2fa-recovery-count"><?php
								echo esc_html(
									sprintf(
										/* translators: %d: remaining recovery codes */
										_n( '%d recovery code remaining', '%d recovery codes remaining', absint( $remaining ), 'securwp' ),
										absint( $remaining )
									)
								);
							?></span>
						<?php endif; ?>
					<?php else : ?>
						<span class="sp2fa-badge sp2fa-badge-off">
							<?php esc_html_e( 'Not enabled', 'securwp' ); ?>
						</span>
						<span class="sp2fa-hint"><?php esc_html_e( 'Add an extra layer of security to your account.', 'securwp' ); ?></span>
					<?php endif; ?>
				</td>
			</tr>

			<?php if ( $is_own ) : ?>

				<?php if ( $is_enabled ) : ?>
					<!-- Enabled state: manage recovery codes, disable -->
					<tr id="sp2fa-actions-row">
						<th><?php esc_html_e( 'Manage', 'securwp' ); ?></th>
						<td>
							<div class="sp2fa-btn-group">
								<button type="button" class="button" id="sp2fa-btn-regen">
									<svg width="14" height="14" viewBox="0 0 16 16" fill="none" style="vertical-align:-2px;margin-right:4px;"><path d="M2 8a6 6 0 0110.89-3.48M14 8a6 6 0 01-10.89 3.48" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><path d="M14 2v4h-4M2 14v-4h4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
									<?php esc_html_e( 'Regenerate Recovery Codes', 'securwp' ); ?>
								</button>
								<button type="button" class="button sp2fa-btn-danger" id="sp2fa-btn-show-disable"><?php esc_html_e( 'Disable 2FA', 'securwp' ); ?></button>
							</div>
							<div id="sp2fa-new-codes" style="display:none;"></div>
						</td>
					</tr>

					<tr id="sp2fa-regen-row" style="display:none;">
						<th><label for="sp2fa-regen-pw"><?php esc_html_e( 'Confirm', 'securwp' ); ?></label></th>
						<td>
							<div class="sp2fa-card sp2fa-card-warn" style="max-width:420px;">
								<p style="margin:0 0 10px;font-weight:500;"><?php esc_html_e( 'Generate a new set of recovery codes?', 'securwp' ); ?></p>
								<p class="description" style="margin-bottom:10px;"><?php esc_html_e( 'Enter your password to replace the current recovery codes. Your existing recovery codes will stop working immediately.', 'securwp' ); ?></p>
								<input type="password" id="sp2fa-regen-pw" class="regular-text" autocomplete="current-password" placeholder="<?php esc_attr_e( 'Enter your password', 'securwp' ); ?>" />
								<div id="sp2fa-regen-msg" class="sp2fa-msg" style="display:none;"></div>
								<div class="sp2fa-actions-bar">
									<button type="button" class="button" id="sp2fa-btn-confirm-regen"><?php esc_html_e( 'Generate New Recovery Codes', 'securwp' ); ?></button>
									<button type="button" class="button" id="sp2fa-btn-cancel-regen"><?php esc_html_e( 'Cancel', 'securwp' ); ?></button>
								</div>
							</div>
						</td>
					</tr>

					<tr id="sp2fa-disable-row" style="display:none;">
						<th><label for="sp2fa-disable-pw"><?php esc_html_e( 'Confirm', 'securwp' ); ?></label></th>
						<td>
							<div class="sp2fa-card sp2fa-card-error" style="max-width:420px;">
								<p style="margin:0 0 10px;font-weight:500;"><?php esc_html_e( 'Are you sure you want to disable two-factor authentication?', 'securwp' ); ?></p>
								<p class="description" style="margin-bottom:10px;"><?php esc_html_e( 'This will make your account less secure. Enter your password to confirm.', 'securwp' ); ?></p>
								<input type="password" id="sp2fa-disable-pw" class="regular-text" autocomplete="current-password" placeholder="<?php esc_attr_e( 'Enter your password', 'securwp' ); ?>" />
								<div id="sp2fa-disable-msg" class="sp2fa-msg" style="display:none;"></div>
								<div class="sp2fa-actions-bar">
									<button type="button" class="button sp2fa-btn-danger" id="sp2fa-btn-disable"><?php esc_html_e( 'Disable 2FA', 'securwp' ); ?></button>
									<button type="button" class="button" id="sp2fa-btn-cancel-disable"><?php esc_html_e( 'Cancel', 'securwp' ); ?></button>
								</div>
							</div>
						</td>
					</tr>
				<?php else : ?>
					<!-- Not enabled: setup flow -->
					<tr id="sp2fa-actions-row">
						<th></th>
						<td>
							<button type="button" class="button button-primary" id="sp2fa-btn-setup">
								<svg width="14" height="14" viewBox="0 0 16 16" fill="none" style="vertical-align:-2px;margin-right:4px;"><rect x="3" y="1" width="10" height="14" rx="2" stroke="currentColor" stroke-width="1.3"/><circle cx="8" cy="8" r="2.5" stroke="currentColor" stroke-width="1.3"/><path d="M8 5.5V4M8 12v-1.5M10.5 8H12M4 8h1.5" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/></svg>
								<?php esc_html_e( 'Set Up Two-Factor Authentication', 'securwp' ); ?>
							</button>
						</td>
					</tr>

					<tr id="sp2fa-setup-row" style="display:none;">
						<th><?php esc_html_e( 'Choose Method', 'securwp' ); ?></th>
						<td>
							<p class="sp2fa-step-desc" style="margin-bottom:12px;"><?php esc_html_e( 'Select your preferred verification method:', 'securwp' ); ?></p>
							<div class="sp2fa-method-cards">
								<?php if ( $allow_totp ) : ?>
								<button type="button" class="sp2fa-method-card" id="sp2fa-choose-totp">
									<svg width="28" height="28" viewBox="0 0 28 28" fill="none">
										<rect x="4" y="2" width="20" height="24" rx="3" stroke="#2563eb" stroke-width="1.5" fill="#eff6ff"/>
										<circle cx="14" cy="14" r="5" stroke="#2563eb" stroke-width="1.5"/>
										<path d="M14 11v3l2 1.5" stroke="#2563eb" stroke-width="1.5" stroke-linecap="round"/>
									</svg>
									<strong><?php esc_html_e( 'Authenticator App', 'securwp' ); ?></strong>
									<span><?php esc_html_e( 'Google Authenticator, Authy, 1Password, etc.', 'securwp' ); ?></span>
									<em class="sp2fa-recommended"><?php esc_html_e( 'Recommended', 'securwp' ); ?></em>
								</button>
								<?php endif; ?>
								<?php if ( $allow_email ) : ?>
								<button type="button" class="sp2fa-method-card" id="sp2fa-choose-email">
									<svg width="28" height="28" viewBox="0 0 28 28" fill="none">
										<rect x="3" y="6" width="22" height="16" rx="2.5" stroke="#6b7280" stroke-width="1.5" fill="#f9fafb"/>
										<path d="M3 8l11 7 11-7" stroke="#6b7280" stroke-width="1.5"/>
									</svg>
									<strong><?php esc_html_e( 'Email Code', 'securwp' ); ?></strong>
									<span><?php esc_html_e( 'Receive a code via email each time you log in.', 'securwp' ); ?></span>
								</button>
								<?php endif; ?>
							</div>
							<div class="sp2fa-setup-password">
								<label for="sp2fa-setup-pw"><?php esc_html_e( 'Confirm your password', 'securwp' ); ?></label>
								<input type="password" id="sp2fa-setup-pw" class="regular-text" autocomplete="current-password" placeholder="<?php esc_attr_e( 'Enter your password to continue', 'securwp' ); ?>" />
								<p class="description"><?php esc_html_e( 'Password confirmation is required before a new verification method can be enrolled.', 'securwp' ); ?></p>
								<div id="sp2fa-setup-msg" class="sp2fa-msg" style="display:none;"></div>
							</div>
							<p style="margin-top:10px;">
								<button type="button" class="button" id="sp2fa-btn-cancel"><?php esc_html_e( 'Cancel', 'securwp' ); ?></button>
							</p>
						</td>
					</tr>

					<tr id="sp2fa-confirm-row" style="display:none;">
						<th><?php esc_html_e( 'Setup', 'securwp' ); ?></th>
						<td id="sp2fa-confirm-content"></td>
					</tr>

					<tr id="sp2fa-codes-row" style="display:none;">
						<th></th>
						<td id="sp2fa-codes-content"></td>
					</tr>
				<?php endif; ?>

			<?php endif; ?>
		</table>
		</div>

		<style>
			/* ── Status badges ──────────────────────────────────── */
			.sp2fa-badge { display:inline-flex; align-items:center; gap:4px; padding:3px 10px; border-radius:20px; font-size:13px; font-weight:600; line-height:1.4; }
			.sp2fa-badge-on { background:#d1fae5; color:#047857; }
			.sp2fa-badge-off { background:#f3f4f6; color:#6b7280; }
			.sp2fa-method-label { margin-left:8px; color:#6b7280; font-size:13px; }
			.sp2fa-recovery-count { display:block; margin-top:6px; color:#6b7280; font-size:12px; }
			.sp2fa-hint { display:block; margin-top:4px; color:#9ca3af; font-size:13px; }

			/* ── Buttons ────────────────────────────────────────── */
			.sp2fa-btn-group { display:flex; gap:8px; flex-wrap:wrap; align-items:flex-start; max-width:100%; }
			.sp2fa-btn-group .button,
			.sp2fa-actions-bar .button {
				display:inline-flex;
				align-items:center;
				justify-content:center;
				max-width:100%;
				white-space:normal;
				height:auto;
				min-height:30px;
				line-height:1.35;
				padding-top:6px;
				padding-bottom:6px;
				text-align:center;
			}
			.sp2fa-btn-danger { color:#dc2626 !important; border-color:#fca5a5 !important; }
			.sp2fa-btn-danger:hover { background:#fef2f2 !important; border-color:#dc2626 !important; }

			/* ── Method selection cards ──────────────────────────── */
			.sp2fa-method-cards { display:flex; gap:12px; flex-wrap:wrap; }
			.sp2fa-method-card {
				display:flex; flex-direction:column; align-items:flex-start; gap:4px;
				width:220px; padding:16px 18px; border:2px solid #e5e7eb; border-radius:10px;
				background:#fff; cursor:pointer; text-align:left; transition:border-color .15s, box-shadow .15s;
				font-family:inherit; font-size:13px; line-height:1.4;
			}
			.sp2fa-method-card:hover { border-color:#2563eb; box-shadow:0 0 0 3px rgba(37,99,235,.1); }
			.sp2fa-method-card strong { font-size:14px; color:#111827; margin-top:4px; }
			.sp2fa-method-card span { color:#6b7280; font-size:12px; }
			.sp2fa-recommended { font-style:normal; font-size:11px; font-weight:600; color:#059669; background:#ecfdf5; padding:1px 8px; border-radius:10px; margin-top:4px; }
			.sp2fa-setup-password { max-width:360px; margin-top:16px; }
			.sp2fa-setup-password label { display:block; margin-bottom:6px; font-weight:600; color:#374151; }
			.sp2fa-setup-password .description { margin-top:6px; }

			/* ── Cards ──────────────────────────────────────────── */
			.sp2fa-card { padding:14px 16px; border-radius:8px; margin-bottom:14px; font-size:13px; line-height:1.5; }
			.sp2fa-card-warn { background:#fffbeb; border:1px solid #fde68a; }
			.sp2fa-card-info { background:#eff6ff; border:1px solid #bfdbfe; }
			.sp2fa-card-error { background:#fef2f2; border:1px solid #fecaca; }
			.sp2fa-card-header { display:flex; align-items:center; gap:8px; margin-bottom:6px; font-size:14px; }
			.sp2fa-card-desc { margin:0; color:#6b7280; font-size:13px; }
			.sp2fa-card-warn .sp2fa-card-header { color:#92400e; }
			.sp2fa-card-warn .sp2fa-card-desc { color:#a16207; }
			.sp2fa-card-info .sp2fa-card-header { color:#1e40af; }
			.sp2fa-card-info .sp2fa-card-desc { color:#1e40af; }

			/* ── QR code & TOTP setup ───────────────────────────── */
			.sp2fa-setup-totp, .sp2fa-setup-email { max-width:480px; }
			.sp2fa-step-label { font-weight:600; font-size:14px; color:#111827; margin:0 0 4px; }
			.sp2fa-step-desc { color:#6b7280; font-size:13px; margin:0 0 12px; }
			.sp2fa-qr-wrapper {
				display:flex; justify-content:center; padding:20px;
				background:#fff; border:2px solid #e5e7eb; border-radius:12px;
				margin-bottom:16px;
			}
			.sp2fa-qr-img { display:block; image-rendering:pixelated; }
			.sp2fa-manual-toggle { margin-bottom:16px; }
			.sp2fa-manual-toggle summary { cursor:pointer; color:#2563eb; font-size:13px; font-weight:500; }
			.sp2fa-manual-toggle summary:hover { text-decoration:underline; }
			.sp2fa-manual-key {
				display:flex; align-items:center; gap:8px;
				margin-top:8px; padding:10px 14px;
				background:#f9fafb; border:1px solid #e5e7eb; border-radius:6px;
			}
			.sp2fa-manual-key code { font-family:'SF Mono',Consolas,'Courier New',monospace; font-size:15px; letter-spacing:2px; color:#111827; user-select:all; flex:1; }
			.sp2fa-copy-btn { display:inline-flex; align-items:center; justify-content:center; width:32px; height:32px; padding:0; border:1px solid #d1d5db; border-radius:6px; background:#fff; color:#6b7280; cursor:pointer; transition:color .15s,border-color .15s; }
			.sp2fa-copy-btn:hover { color:#111827; border-color:#9ca3af; }
			.sp2fa-divider { height:1px; background:#e5e7eb; margin:18px 0; }

			/* ── Verification input ─────────────────────────────── */
			.sp2fa-verify-input { margin-bottom:4px; }
			.sp2fa-verify-input label { display:block; font-weight:600; font-size:13px; color:#374151; margin-bottom:4px; }
			.sp2fa-verify-input input[type="text"] { font-family:'SF Mono',Consolas,'Courier New',monospace; font-size:18px; letter-spacing:6px; width:160px; padding:8px 12px; text-align:center; }

			/* ── Messages ───────────────────────────────────────── */
			.sp2fa-msg { padding:8px 14px; border-radius:6px; font-size:13px; margin-top:10px; }
			.sp2fa-msg.error { background:#fef2f2; color:#dc2626; border:1px solid #fecaca; }
			.sp2fa-msg.success { background:#f0fdf4; color:#059669; border:1px solid #bbf7d0; }

			/* ── Actions bar ────────────────────────────────────── */
			.sp2fa-actions-bar { display:flex; gap:8px; margin-top:14px; flex-wrap:wrap; align-items:flex-start; }

			/* ── Recovery codes grid ────────────────────────────── */
			.sp2fa-codes-grid { display:grid; grid-template-columns:1fr 1fr; gap:6px; margin:10px 0; }
			.sp2fa-code-item { display:block; padding:7px; background:#fff; border:1px solid #e5e7eb; border-radius:4px; text-align:center; font-family:'SF Mono',Consolas,'Courier New',monospace; font-size:13px; }
			.sp2fa-copy-codes { margin-top:4px; }

			/* ── Success banner ──────────────────────────────────── */
			.sp2fa-success-banner { display:flex; align-items:flex-start; gap:12px; padding:14px 16px; background:#ecfdf5; border:1px solid #a7f3d0; border-radius:8px; margin-bottom:16px; }
			.sp2fa-success-banner strong { display:block; color:#047857; font-size:14px; }
			.sp2fa-success-banner p { margin:2px 0 0; color:#059669; font-size:13px; }

			/* ── Loading ────────────────────────────────────────── */
			.sp2fa-loading { display:flex; align-items:center; color:#6b7280; font-size:13px; padding:8px 0; }

			/* ── Subtle link ────────────────────────────────────── */
			.sp2fa-link-subtle { color:#6b7280; font-size:12px; text-decoration:underline; }
			.sp2fa-link-subtle:hover { color:#374151; }
		</style>
		<?php
	}

	/**
	 * Get the default settings for the 2FA module.
	 *
	 * @return array Default settings.
	 */
	public static function get_default_settings(): array {
		return [
			'enabled'              => false,
			'enforce_for_roles'    => [],
			'remember_device_days' => 30,
			'grace_period_days'    => 3,
			'allowed_methods'      => [ 'totp', 'email' ],
		];
	}

	/**
	 * Normalize allowed-method settings to supported values only.
	 *
	 * @param mixed $methods Raw method configuration payload.
	 * @return array<int,string>
	 */
	public static function sanitize_allowed_methods( $methods ): array {
		if ( ! is_array( $methods ) ) {
			return [];
		}

		$valid = array_map(
			'sanitize_key',
			array_values( array_intersect( array_map( 'sanitize_key', $methods ), [ 'totp', 'email' ] ) )
		);

		return array_values( array_unique( array_filter( $valid ) ) );
	}

	/**
	 * Apply runtime side effects after 2FA policy changes.
	 *
	 * Keeps grace-period tracking and pending login sessions aligned whether the
	 * change originates from the plugin UI or a Console command.
	 *
	 * @param array $previous Previous persisted settings.
	 * @param array $current  Newly persisted settings.
	 */
	public static function apply_settings_side_effects( array $previous, array $current ): void {
		$defaults = self::get_default_settings();
		$previous = wp_parse_args( $previous, $defaults );
		$current  = wp_parse_args( $current, $defaults );

		$was_enabled = ! empty( $previous['enabled'] );
		$now_enabled = ! empty( $current['enabled'] );

		if ( $now_enabled && ! $was_enabled ) {
			LoginInterstitial::clear_all_enforcement_starts();
		}

		$previous_methods = self::sanitize_allowed_methods( $previous['allowed_methods'] ?? $defaults['allowed_methods'] );
		$current_methods  = self::sanitize_allowed_methods( $current['allowed_methods'] ?? $defaults['allowed_methods'] );
		if ( empty( $previous_methods ) ) {
			$previous_methods = $defaults['allowed_methods'];
		}
		if ( empty( $current_methods ) ) {
			$current_methods = $defaults['allowed_methods'];
		}
		sort( $previous_methods );
		sort( $current_methods );

		$remember_days_changed = (int) ( $previous['remember_device_days'] ?? $defaults['remember_device_days'] ) !== (int) ( $current['remember_device_days'] ?? $defaults['remember_device_days'] );
		$methods_changed       = $previous_methods !== $current_methods;

		if ( ! $now_enabled || $remember_days_changed || $methods_changed ) {
			self::clear_all_remembered_devices();
		}

		if ( ! $now_enabled ) {
			LoginSessionStore::delete_all();
			return;
		}

		LoginSessionStore::delete_disallowed_methods( $current_methods );
	}

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

		$table_name = $wpdb->prefix . 'securwp_two_factor';
		if ( ! SchemaInspector::table_exists( $table_name ) ) {
			return;
		}

		$table = SchemaInspector::table_identifier( $table_name );
		$now   = time();

		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
		$wpdb->query(
			$wpdb->prepare(
				"UPDATE {$table}
				 SET remember_token = NULL,
				     remember_expires = NULL,
				     updated_at = %d
				 WHERE remember_token IS NOT NULL
				    OR remember_expires IS NOT NULL",
				$now
			)
		);
		// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter
	}

	public static function is_schema_current(): bool {
		global $wpdb;

		$table = $wpdb->prefix . 'securwp_two_factor';
		$method = SchemaInspector::get_column( $table, 'method' );
		if ( ! is_array( $method ) ) {
			return false;
		}

		$method_type = strtolower( (string) ( $method['Type'] ?? '' ) );
		$method_null = strtoupper( (string) ( $method['Null'] ?? '' ) );
		$method_default = SchemaInspector::normalize_default_value( $method['Default'] ?? '' );
		if ( $method_type !== 'varchar(10)' || $method_null !== 'NO' || $method_default !== 'totp' ) {
			return false;
		}

		$totp_secret = SchemaInspector::get_column( $table, 'totp_secret' );
		if ( ! is_array( $totp_secret ) ) {
			return false;
		}

		$totp_type = strtolower( (string) ( $totp_secret['Type'] ?? '' ) );
		$totp_null = strtoupper( (string) ( $totp_secret['Null'] ?? '' ) );

		return $totp_type === 'text' && $totp_null === 'YES';
	}

	/**
	 * Get the current module settings.
	 *
	 * @return array Current settings merged with defaults.
	 */
	public function get_settings(): array {
		return $this->settings;
	}

	/**
	 * Load settings from the database with defaults.
	 */
	private function load_settings(): void {
		$defaults       = self::get_default_settings();
		$this->settings = \SecurWP\Includes\SettingsStore::get( \SecurWP\Includes\SettingsStore::COL_TWOFA, $defaults );
	}
}
