<?php

declare(strict_types=1);

namespace SecurWP\Modules\TwoFactor;

use SecurWP\Core\Database\SchemaInspector;

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

/**
 * Email-based one-time password provider for 2FA.
 *
 * Uses database-backed setup state for authenticated enrollment and shared
 * mail helpers for login-challenge delivery. This keeps setup verification
 * out of transients while preserving a single email rendering path.
 */
class EmailOTPProvider {

	/** Length of the generated numeric code. */
	private const CODE_LENGTH = 6;

	/** Code TTL in seconds (10 minutes). */
	private const CODE_TTL = 600;

	/** Minimum interval between sends in seconds. */
	private const COOLDOWN = 60;

	/** Maximum verification attempts per code. */
	private const MAX_ATTEMPTS = 5;

	/** Advisory lock prefix for setup verification state. */
	private const SETUP_LOCK_PREFIX = 'securwp_email_setup_otp_';

	/**
	 * Generate and deliver an email OTP for an authenticated setup flow.
	 */
	public static function send_setup_code( int $user_id ): bool {
		$user = get_userdata( $user_id );
		if ( ! $user || empty( $user->user_email ) ) {
			return false;
		}

		global $wpdb;
		$lock_name = self::SETUP_LOCK_PREFIX . $user_id;

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Advisory locking requires a database-native GET_LOCK() call.
		$lock = $wpdb->get_var( $wpdb->prepare( 'SELECT GET_LOCK(%s, 2)', $lock_name ) );
		if ( '1' !== (string) $lock ) {
			return false;
		}

		try {
			$state = self::load_setup_state( $user_id );
			if ( ! is_array( $state ) ) {
				return false;
			}

			$sent_at = (int) ( $state['setup_email_sent_at'] ?? 0 );
			if ( ! empty( $state['setup_email_delivered'] ) && $sent_at > 0 && ( time() - $sent_at ) < self::COOLDOWN ) {
				return true;
			}

			$code      = self::generate_code();
			$site_name = get_bloginfo( 'name' ) ?: (string) wp_parse_url( home_url(), PHP_URL_HOST );
			$sent      = self::send_email( $user->user_email, $code, $site_name, 'setup' );

			if ( ! $sent ) {
				return false;
			}

			$now = time();
			$code_hash = self::hash_code_for_storage( $code );
			if ( '' === $code_hash ) {
				return false;
			}

			return self::update_setup_state(
				$user_id,
				[
					'setup_email_code_hash' => $code_hash,
					'setup_email_attempts'  => 0,
					'setup_email_sent_at'   => $now,
					'setup_email_delivered' => 1,
					'updated_at'            => $now,
				]
			);
		} finally {
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Advisory locking requires a database-native RELEASE_LOCK() call.
			$wpdb->get_var( $wpdb->prepare( 'SELECT RELEASE_LOCK(%s)', $lock_name ) );
		}
	}

	/**
	 * Verify a submitted email OTP for an authenticated setup flow.
	 */
	public static function verify_setup_code( int $user_id, string $code ): bool {
		global $wpdb;
		$lock_name = self::SETUP_LOCK_PREFIX . $user_id;
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Advisory locking requires a database-native GET_LOCK() call.
		$lock = $wpdb->get_var( $wpdb->prepare( 'SELECT GET_LOCK(%s, 2)', $lock_name ) );

		if ( '1' !== (string) $lock ) {
			return false;
		}

		try {
			$state = self::load_setup_state( $user_id );
			if ( ! is_array( $state ) || empty( $state['setup_email_code_hash'] ) ) {
				return false;
			}

			$sent_at = (int) ( $state['setup_email_sent_at'] ?? 0 );
			if ( $sent_at <= 0 || ( time() - $sent_at ) > self::CODE_TTL ) {
				self::reset_setup_state( $user_id );
				return false;
			}

			$attempts = (int) ( $state['setup_email_attempts'] ?? 0 );

			if ( $attempts >= self::MAX_ATTEMPTS ) {
				self::reset_setup_state( $user_id );
				return false;
			}

			$stored_hash = (string) $state['setup_email_code_hash'];

			if ( self::verify_stored_code( (string) $code, $stored_hash ) ) {
				self::reset_setup_state( $user_id );
				return true;
			}

			$next_attempts = $attempts + 1;
			if ( $next_attempts >= self::MAX_ATTEMPTS ) {
				self::reset_setup_state( $user_id );
				return false;
			}

			self::update_setup_state(
				$user_id,
				[
					'setup_email_attempts' => $next_attempts,
					'updated_at'           => time(),
				]
			);

			return false;
		} finally {
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Advisory locking requires a database-native RELEASE_LOCK() call.
			$wpdb->get_var( $wpdb->prepare( 'SELECT RELEASE_LOCK(%s)', $lock_name ) );
		}
	}

	/**
	 * Clear any pending authenticated-setup email OTP state for the user.
	 */
	public static function reset_setup_state( int $user_id ): void {
		self::update_setup_state(
			$user_id,
			[
				'setup_email_code_hash' => null,
				'setup_email_attempts'  => 0,
				'setup_email_sent_at'   => null,
				'setup_email_delivered' => 0,
				'updated_at'            => time(),
			]
		);
	}

	/**
	 * Ensure the authenticated-setup email OTP columns exist.
	 */
	public static function is_setup_schema_current(): bool {
		global $wpdb;

		return SchemaInspector::has_columns(
			$wpdb->prefix . 'securwp_two_factor',
			[
				'setup_email_code_hash',
				'setup_email_attempts',
				'setup_email_sent_at',
				'setup_email_delivered',
			]
		);
	}

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

		$table_name = $wpdb->prefix . 'securwp_two_factor';

		return self::is_setup_schema_current()
			&& self::column_supports_password_hash_storage( $table_name, 'setup_email_code_hash' );
	}

	public static function generate_plaintext_code(): string {
		return self::generate_code();
	}

	public static function hash_code_for_storage( string $code ): string {
		$hash = password_hash( $code, PASSWORD_DEFAULT );

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

	public static function verify_stored_code( string $code, string $stored_hash ): bool {
		if ( '' === $stored_hash ) {
			return false;
		}

		$hash_info = password_get_info( $stored_hash );
		if ( empty( $hash_info['algo'] ) ) {
			return false;
		}

		return password_verify( $code, $stored_hash );
	}

	public static function column_supports_password_hash_storage( string $table_name, string $column_name ): bool {
		$column = SchemaInspector::get_column( $table_name, $column_name );
		if ( ! is_array( $column ) ) {
			return false;
		}

		$type = strtolower( (string) ( $column['Type'] ?? '' ) );
		if ( in_array( $type, [ 'text', 'mediumtext', 'longtext' ], true ) ) {
			return true;
		}

		if ( preg_match( '/^varchar\((\d+)\)$/', $type, $matches ) ) {
			return (int) ( $matches[1] ?? 0 ) >= 255;
		}

		return false;
	}

	public static function send_challenge_code( int $user_id, string $code, string $context = 'login' ): bool {
		$user = get_userdata( $user_id );
		if ( ! $user || empty( $user->user_email ) ) {
			return false;
		}

		$site_name = get_bloginfo( 'name' ) ?: (string) wp_parse_url( home_url(), PHP_URL_HOST );
		return self::send_email( $user->user_email, $code, $site_name, $context );
	}

	public static function cooldown_seconds(): int {
		return self::COOLDOWN;
	}

	public static function max_attempts(): int {
		return self::MAX_ATTEMPTS;
	}

	/**
	 * Load the current authenticated-setup OTP state for a user.
	 *
	 * @return array<string,mixed>|null
	 */
	private static function load_setup_state( int $user_id ): ?array {
		global $wpdb;

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

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

		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
		$row = $wpdb->get_row(
			$wpdb->prepare(
				"SELECT user_id, setup_email_code_hash, setup_email_attempts, setup_email_sent_at, setup_email_delivered
				 FROM {$table}
				 WHERE user_id = %d
				 LIMIT 1",
				$user_id
			),
			ARRAY_A
		);
		// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter

		if ( ! is_array( $row ) ) {
			return null;
		}

		$row['setup_email_attempts']  = (int) ( $row['setup_email_attempts'] ?? 0 );
		$row['setup_email_sent_at']   = (int) ( $row['setup_email_sent_at'] ?? 0 );
		$row['setup_email_delivered'] = ! empty( $row['setup_email_delivered'] );

		return $row;
	}

	/**
	 * Persist authenticated-setup OTP state to the plugin-owned 2FA row.
	 *
	 * @param array<string,mixed> $fields Fields to update.
	 */
	private static function update_setup_state( int $user_id, array $fields ): bool {
		global $wpdb;

		$table_name = $wpdb->prefix . 'securwp_two_factor';
		if ( ! SchemaInspector::table_exists( $table_name ) || ! self::is_setup_schema_current() || empty( $fields ) ) {
			return false;
		}

		$formats = [];
		foreach ( $fields as $value ) {
			if ( is_int( $value ) ) {
				$formats[] = '%d';
			} elseif ( is_null( $value ) ) {
				$formats[] = null;
			} else {
				$formats[] = '%s';
			}
		}

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Updates a single row in a plugin-owned table.
		$updated = $wpdb->update(
			$table_name,
			$fields,
			[ 'user_id' => $user_id ],
			$formats,
			[ '%d' ]
		);

		return false !== $updated;
	}

	/**
	 * Generate a cryptographically random 6-digit numeric code.
	 *
	 * @return string Zero-padded 6-digit code.
	 */
	private static function generate_code(): string {
		return str_pad( (string) random_int( 0, 999999 ), self::CODE_LENGTH, '0', STR_PAD_LEFT );
	}

	/**
	 * Render the email template and send via wp_mail.
	 *
	 * @param string $email     Recipient email address.
	 * @param string $code      The plaintext OTP code.
	 * @param string $site_name Site name for display.
	 * @param string $context   'login' or 'setup'.
	 * @return bool Whether wp_mail() reported success.
	 */
	private static function send_email( string $email, string $code, string $site_name, string $context = 'login' ): bool {
		if ( $context === 'setup' ) {
			$subject = '[SecurWP] Verify Your Two-Factor Authentication Setup';
			$heading = __( 'Two-Factor Authentication Setup', 'securwp' );
			$intro   = sprintf(
				/* translators: %s: site name */
				__( 'Use the following code to verify your two-factor authentication setup on %s:', 'securwp' ),
				$site_name
			);
			$warning = __( 'If you did not initiate this setup, you can safely ignore this email.', 'securwp' );
		} else {
			$subject = '[SecurWP] Your Login Verification Code';
			$heading = __( 'Login Verification Code', 'securwp' );
			$intro   = sprintf(
				/* translators: %s: site name */
				__( 'Use the following code to complete your login to %s:', 'securwp' ),
				$site_name
			);
			$warning = __( 'If you did not attempt to log in, someone may have your password. Consider changing it immediately.', 'securwp' );
		}

		$data = [
			'code'            => $code,
			'site_name'       => $site_name,
			'site_url'        => home_url(),
			'expires_minutes' => (int) ceil( self::CODE_TTL / 60 ),
			'heading'         => $heading,
			'intro'           => $intro,
			'warning'         => $warning,
		];

		// Render inner template.
		$template_path = __DIR__ . '/templates/email-otp.php';
		ob_start();
		include $template_path;
		$inner_content = ob_get_clean();

		// Wrap in base template.
		$base_path = dirname( __DIR__ ) . '/Notifications/templates/base.php';
		ob_start();
		$content = $inner_content;
		include $base_path;
		$html = (string) ob_get_clean();

		add_filter( 'wp_mail_content_type', [ self::class, 'html_content_type' ] );
		$sent = wp_mail( $email, sanitize_text_field( $subject ), $html );
		remove_filter( 'wp_mail_content_type', [ self::class, 'html_content_type' ] );

		return $sent;
	}

	/**
	 * HTML content type callback for wp_mail.
	 *
	 * @return string
	 */
	public static function html_content_type(): string {
		return 'text/html';
	}
}
