<?php

declare(strict_types=1);

namespace SiteFort\Modules\TwoFactor;

use SiteFort\Core\Database\SchemaInspector;
use SiteFort\Modules\Notifications\NotificationService;

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 = 'sitefort_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 . 'sitefort_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 . 'sitefort_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 . 'sitefort_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 . 'sitefort_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 OTP body through the global email template and send it.
	 *
	 * @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 delivery reported success.
	 */
	private static function send_email( string $email, string $code, string $site_name, string $context = 'login' ): bool {
		if ( $context === 'setup' ) {
			$subject_label = __( 'Verify Your Two-Factor Authentication Setup', 'sitefort' );
			$heading = __( 'Two-Factor Authentication Setup', 'sitefort' );
			$intro   = sprintf(
				/* translators: %s: site name */
				__( 'Use the following code to verify your two-factor authentication setup on %s:', 'sitefort' ),
				$site_name
			);
			$warning = __( 'If you did not initiate this setup, you can safely ignore this email.', 'sitefort' );
		} else {
			$subject_label = __( 'Your Login Verification Code', 'sitefort' );
			$heading = __( 'Login Verification Code', 'sitefort' );
			$intro   = sprintf(
				/* translators: %s: site name */
				__( 'Use the following code to complete your login to %s:', 'sitefort' ),
				$site_name
			);
			$warning = __( 'If you did not attempt to log in, someone may have your password. Consider changing it immediately.', 'sitefort' );
		}

		$data = NotificationService::build_template_data(
			$heading,
			[
				'code'            => $code,
				'site_name'       => $site_name,
				'site_url'        => home_url(),
				'expires_minutes' => (int) ceil( self::CODE_TTL / 60 ),
				'heading'         => $heading,
				'intro'           => $intro,
				'warning'         => $warning,
			]
		);
		$subject = sprintf( '[%s] %s', (string) $data['brand_name'], $subject_label );

		$template_path = __DIR__ . '/templates/email-otp.php';
		$html = NotificationService::render_template_file( $template_path, $data );

		return NotificationService::send_html_mail( $email, $subject, $html, (string) $data['email_from_name'] );
	}
}
