<?php
/**
 * Class for creating an email provider.
 *
 * @package Two_Factor
 */

namespace RSSSL\Security\WordPress\Two_Fa\Providers;

/**
 * Class for creating an email provider.
 *
 * @since 7.0.6
 *
 * @package Two_Factor
 */
require_once rsssl_path . 'mailer/class-mail.php';

use RSSSL\Security\WordPress\Two_Fa\Controllers\Rsssl_Email_Controller;
use RSSSL\Security\WordPress\Two_Fa\Rsssl_Two_Factor_Settings;
use rsssl_mailer;
use Exception;
use WP_User;

/**
 * Generate and email the user token.
 *
 * @param WP_User $user WP_User object of the logged-in user.
 *
 * @return void
 * @since 0.1-dev
 */
class Rsssl_Two_Factor_Email extends Rsssl_Two_Factor_Provider implements Rsssl_Two_Factor_Provider_Interface {

	/**
	 * The user meta token key.
	 *
	 * @var string
	 */
	public const RSSSL_TOKEN_META_KEY = '_rsssl_factor_email_token';

	/**
	 * Store the timestamp when the token was generated.
	 *
	 * @var string
	 */
	public const RSSSL_TOKEN_META_KEY_TIMESTAMP = '_rsssl_factor_email_token_timestamp';

	/**
	 * Name of the input field used for code resend.
	 *
	 * @var string
	 */
	public const RSSSL_INPUT_NAME_RESEND_CODE = 'rsssl-two-factor-email-code-resend';
	public const SECRET_META_KEY              = 'rsssl_two_fa_email_enabled';

	public const METHOD = 'email';

	public const NAME = 'Email';

	/**
	 * Ensures only one instance of this class exists in memory at any one time.
	 *
	 * @since 0.1-dev
	 */
	public static function get_instance() {
		static $instance;
		$class = __CLASS__;
		if ( ! is_a( $instance, $class ) ) {
			$instance = new $class();
		}
		return $instance;
	}

	/**
	 * Class constructor.
	 *
	 * @since 0.1-dev
	 */
	protected function __construct() {
		add_action( 'rsssl_two_factor_user_options_' . __CLASS__, array( $this, 'user_options' ) );
		parent::__construct();
	}

    /**
     * Starts the corresponding controller
     * @return void
     */
    public static function start_controller(string $namespace, string $version, string $featureVersion ): void {
        new Rsssl_Email_Controller($namespace, $version, $featureVersion);
    }

    /**
     * Ensure PHP session is started.
     */
    public static function ensure_session_started() {
        if ( PHP_SAPI !== 'cli' && ! headers_sent() && ! session_id() ) {
            session_start();
        }
    }

	/**
	 * Returns the name of the provider.
	 *
	 * @since 0.1-dev
	 */
	public function get_label(): string {
		return _x( 'Email', 'Provider Label', 'really-simple-ssl' );
	}

	/**
	 * Generate the user token.
	 *
	 * @param int $user_id User ID.
	 *
	 * @return string
	 * @since 0.1-dev
	 */
	public function generate_token( int $user_id ): string {
		$token = self::get_code();

		update_user_meta( $user_id, self::RSSSL_TOKEN_META_KEY_TIMESTAMP, time() );
		update_user_meta( $user_id, self::RSSSL_TOKEN_META_KEY, wp_hash( $token ) );

		return $token;
	}

	/**
	 * Check if user has a valid token already.
	 *
	 * @param int $user_id User ID.
	 *
	 * @return boolean      If user has a valid email token.
	 */
	public function user_has_token( int $user_id ): bool {
		$hashed_token = $this->get_user_token( $user_id );

		if ( ! empty( $hashed_token ) ) {
			return true;
		}

		return false;
	}

	/**
	 * Has the user token validity timestamp expired.
	 *
	 * @param integer $user_id User ID.
	 *
	 * @return boolean
	 */
	public function user_token_has_expired( int $user_id ): bool {

		$token_lifetime = $this->user_token_lifetime( $user_id );
		$token_ttl      = $this->user_token_ttl( $user_id );

		// Invalid token lifetime is considered an expired token.
		return ! ( is_int( $token_lifetime ) && $token_lifetime <= $token_ttl );
	}

	/**
	 * Get the lifetime of a user token in seconds.
	 *
	 * @param integer $user_id User ID.
	 *
	 * @return integer|null Return `null` if the lifetime can't be measured.
	 */
	public function user_token_lifetime( $user_id ) {
		$timestamp = (int) get_user_meta( $user_id, self::RSSSL_TOKEN_META_KEY_TIMESTAMP, true );

		if ( ! empty( $timestamp ) ) {
			return time() - $timestamp;
		}

		return null;
	}

	/**
	 * Return the token time-to-live for a user.
	 *
	 * @param integer $user_id User ID.
	 *
	 * @return integer
	 */
	public function user_token_ttl( int $user_id ): int {
		$token_ttl = 15 * MINUTE_IN_SECONDS;

		/**
		 * Number of seconds the token is considered valid
		 * after the generation.
		 *
		 * @param integer $token_ttl Token time-to-live in seconds.
		 * @param integer $user_id User ID.
		 */
		return (int) apply_filters( 'rsssl_two_factor_token_ttl', $token_ttl, $user_id );
	}

	/**
	 * Get the authentication token for the user.
	 *
	 * @param int $user_id User ID.
	 *
	 * @return string|boolean  User token or `false` if no token found.
	 */
	public function get_user_token( int $user_id ) {
		$hashed_token = get_user_meta( $user_id, self::RSSSL_TOKEN_META_KEY, true );

		if ( ! empty( $hashed_token ) && is_string( $hashed_token ) ) {
			return $hashed_token;
		}

		return false;
	}

	/**
	 * Validate the user token.
	 *
	 * @param int    $user_id User ID.
	 * @param string $token   User token.
	 *
	 * @return boolean
	 * @since 0.1-dev
	 */
	public function validate_token( int $user_id, string $token ): bool {

		$hashed_token = $this->get_user_token( $user_id );

		// Bail if token is empty or it doesn't match.
		if ( empty( $hashed_token ) || ! hash_equals( wp_hash( $token ), $hashed_token ) ) {
			return false;
		}

		if ( $this->user_token_has_expired( $user_id ) ) {
			return false;
		}

		// Ensure the token can be used only once.
		$this->delete_token( $user_id );

		update_user_meta( $user_id, 'rsssl_two_fa_status_email', 'active' );

		return true;
	}

	/**
	 * Delete the user token.
	 *
	 * @param int $user_id User ID.
	 *
	 * @since 0.1-dev
	 */
	public function delete_token( int $user_id ): void {
		delete_user_meta( $user_id, self::RSSSL_TOKEN_META_KEY );
	}

	/**
	 * Generate and email the user token.
	 *
	 * @param WP_User $user WP_User object of the logged-in user.
	 *
	 * @return void
	 * @since 0.1-dev
	 */
	public function generate_and_email_token( WP_User $user, $profile = false, $is_resend = false ): void {
        self::ensure_session_started();
        $token = $this->generate_token( $user->ID );

        $skip_two_fa_url = Rsssl_Two_Factor_Settings::rsssl_one_time_login_url( $user->ID, false, $profile );
		// Add skip button to email content.
		$skip_button_html = sprintf(
			'<a href="%s" class="button" style="padding: 10px 30px; background: #2A7ABF; border-color: #2A7ABF; color: #fff; text-decoration: none; text-shadow: none; display: inline-block; margin-top: 15px; font-size: 0.8125rem; font-weight: 300; transition: all .3s ease; min-height: 10px;">' . __( 'Continue', 'really-simple-ssl' ) . '</a>',
			esc_url( $skip_two_fa_url )
		);

		/* translators: %s: site name */
		$subject = wp_strip_all_tags( sprintf( __( 'Your login confirmation code for %s', 'really-simple-ssl' ), wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES ) ) );
		/* translators: %s: token */
		$token_cleaned = wp_strip_all_tags( $token );

		// insert whitespace after four characters in the $token, for readability.
		$token_cleaned = preg_replace( '/(.{4})/', '$1 ', $token_cleaned );

		$token_html = sprintf(
			'
<table cellspacing="0" cellpadding="0" border="0" width="100%%" style="margin-top: 25px;background-color:white; box-shadow: 1px 3px 0 1px rgba(211, 211, 211, 0.3); height: 180px;"> <!-- Further increased height for white box -->
    <tr>
        <td style="padding: 45px 10px 10px 10px; vertical-align: middle; font-size: 18px; font-weight:700; text-align: center;">%s</td> <!-- Increased padding for top and bottom -->
    </tr>
    <tr>
        <td style="padding: 10px 20px 45px 20px; vertical-align: middle; text-align: center;">%s</td> <!-- Increased padding for bottom -->
    </tr>
</table>',
			$token_cleaned,
			$skip_button_html
		);

        if($profile) {
            $message = sprintf(
                __( "Below you'll find the email activation code for %1\$s. It's valid for 15 minutes. %2\$s", 'really-simple-ssl' ),
                site_url(),
                $token_html
            );
        } else {
            $message = sprintf(
                __( "Below you will find your login code for %1\$s. It's valid for 15 minutes. %2\$s", 'really-simple-ssl' ),
                site_url(),
                $token_html
            );
        }


		/**
		 * Filter the token email subject.
		 *
		 * @param string $subject The email subject line.
		 * @param int    $user_id The ID of the user.
		 */
		$subject = apply_filters( 'rsssl_two_factor_token_email_subject', $subject, $user->ID );

		/**
		 * Filter the token email message.
		 *
		 * @param string $message The email message.
		 * @param string $token   The token.
		 * @param int    $user_id The ID of the user.
		 */
		$message = apply_filters( 'rsssl_two_factor_token_email_message', $message, $token, $user->ID );

		if ( ! class_exists( 'rsssl_mailer' ) ) {
			require_once rsssl_path . 'mailer/class-mail.php';
		}

		$mailer          = new rsssl_mailer();
		$mailer->subject = $subject;
		$mailer->branded = false;
		/* translators: %s is replaced with the site url */
		$mailer->sent_by_text      = "<b>" . sprintf( __( 'Notification by %s', 'really-simple-ssl' ), site_url() ) . "</b>";
		$mailer->template_filename = apply_filters( 'rsssl_email_template', rsssl_path . '/mailer/templates/email-unbranded.html' );
		$mailer->to                = $user->user_email;
		$mailer->title             = __( 'Hi', 'really-simple-ssl' ) . ' ' . $user->display_name . ',';
		$mailer->message           = $message;
		$mailer->send_mail();

        if ( $is_resend ) {
            $_SESSION['rsssl_email_resent'] = true;
        }
	}

    public static function maybe_show_email_resend_notice( $user ) {
        self::ensure_session_started();
        if ( ! empty( $_SESSION['rsssl_email_resent'] ) ) {
            echo '<div class="notice notice-success" style="margin-bottom:16px;"><p>' . esc_html__( 'A new verification code has been sent to your email address.', 'really-simple-ssl' ) . '</p></div>';
            unset( $_SESSION['rsssl_email_resent'] );
        }
    }

	/**
	 * Prints the form that prompts the user to authenticate.
	 *
	 * @param WP_User $user WP_User object of the logged-in user.
	 *
	 * @since 0.1-dev
	 */
	public function authentication_page( WP_User $user ): void {

		if ( ! $user ) {
			return;
		}

		if ( ! $this->user_has_token( $user->ID ) || $this->user_token_has_expired( $user->ID ) ) {
			$this->generate_and_email_token( $user );
		}

		require_once ABSPATH . '/wp-admin/includes/template.php';
		?>
		<p class="two-factor-prompt"><?php esc_html_e( 'A verification code has been sent to the email address associated with your account.', 'really-simple-ssl' ); ?></p>
		<p>
			<label for="rsssl-authcode"><?php esc_html_e( 'Verification Code:', 'really-simple-ssl' ); ?></label>
			<input type="text" inputmode="numeric" name="rsssl-two-factor-email-code" id="rsssl-authcode" class="input rsssl-authcode" value="" size="20" pattern="[0-9 ]*" placeholder="1234 5678" data-digits="8" />
			<?php submit_button( __( 'Log In', 'really-simple-ssl' ) ); ?>
		</p>
		<p class="rsssl-two-factor-email-resend">
			<input type="submit" class="button" name="<?php echo esc_attr( self::RSSSL_INPUT_NAME_RESEND_CODE ); ?>" value="<?php esc_attr_e( 'Resend Code', 'really-simple-ssl' ); ?>" />
		</p>
		<script type="text/javascript">
			setTimeout( function(){
				var d;
				try{
					d = document.getElementById('rsssl-authcode');
					d.value = '';
					d.focus();
				} catch(e){}
			}, 200);
		</script>
		<?php
		$provider = get_user_meta( $user->ID, 'rsssl_two_fa_status_email', true );

		foreach ( $user->roles as $role ) {
			// Never show the skip link if a role is a forced role.
			$two_fa_forced_roles = is_array(rsssl_get_option('two_fa_forced_roles'))
				? rsssl_get_option('two_fa_forced_roles')
				: [];

			if (in_array($role, $two_fa_forced_roles, true)) {
				break;
			}

			// If optional and open, allow the user to skip 2FA for now.
			if ( 'open' === $provider && in_array( $role, rsssl_get_option( 'two_fa_enabled_roles_email', array() ), true ) ) {

				$skip_two_fa_url = Rsssl_Two_Factor_Settings::rsssl_one_time_login_url( $user->ID, true );

				?>
				<a class="rsssl-skip-link" href="<?php echo esc_url( $skip_two_fa_url ); ?>" style="display: flex; justify-content: center; margin: 15px 20px 0 0;">
					<?php esc_html_e( "Don't use Two-Factor Authentication", 'really-simple-ssl' ); ?>
				</a>
				<?php

			}
		}
	}

	/**
	 * Send the email code if missing or requested. Stop the authentication
	 * validation if a new token has been generated and sent.
	 *
	 * @param  WP_USer $user WP_User object of the logged-in user.
	 * @return boolean
	 */
	public function pre_process_authentication( $user ): bool {
		if ( isset( $user->ID ) && isset( $_REQUEST[ self::RSSSL_INPUT_NAME_RESEND_CODE ] ) ) {
			$this->generate_and_email_token( $user, false, true );
			return true;
		}

		return false;
	}

	/**
	 * Validates the users input token.
	 *
	 * @param WP_User $user WP_User object of the logged-in user.
	 * @return boolean
	 *@since 0.1-dev
	 *
	 */
	public function validate_authentication( WP_User $user ): bool {

		$code = self::sanitize_code_from_request( 'rsssl-two-factor-email-code' );
		if ( ! isset( $user->ID ) || ! $code ) {
			return false;
		}

		return $this->validate_token( $user->ID, $code );
	}

	/**
	 * Whether this Two Factor provider is configured and available for the user specified.
	 *
	 * @param WP_User $user WP_User object of the logged-in user.
	 * @return boolean
	 *@since 0.1-dev
	 *
	 */
	public function is_available_for_user( WP_User $user ): bool {
		return true;
	}

	/**
	 * Inserts markup at the end of the user profile field for this provider.
	 *
	 * @param WP_User $user WP_User object of the logged-in user.
	 *
	 * @since 0.1-dev
	 */
	public function user_options( WP_User $user ): void {
		$email = $user->user_email;
		?>
		<div>
			<?php
			echo esc_html(
				sprintf(
				/* translators: %s: email address */
					__( 'Authentication codes will be sent to %s.', 'really-simple-ssl' ),
					$email
				)
			);
			?>
		</div>
		<?php
	}

	/**
	 * Check if the user is forced to use two-factor authentication.
	 *
	 * @param  WP_User $user  The user object.
	 *
	 * @return bool Whether the user is forced to use two-factor authentication.
	 */
	public static function is_forced( WP_User $user ): bool {
		// If there is no user logged in, it can't check if the user is forced.
		if ( ! $user->exists() ) {
			return false;
		}
		return Rsssl_Two_Factor_Settings::get_role_status( 'email', $user->ID ) === 'forced';
	}

	/**
	 * Check if a user is Optional.
	 *
	 * @param  WP_User $user  The user object.
	 *
	 * @return bool Whether the user is optional or not.
	 */
	public static function is_optional( WP_User $user ): bool {
		if ( ! $user->exists() ) {
			return false;
		}

		if ( 'disabled' === Rsssl_Two_Factor_Settings::get_user_status( 'email', $user->ID ) ) {
			return false;
		}

		$optional_roles = (array) rsssl_get_option( 'two_fa_enabled_roles_email', array() );
		$user_roles     = $user->roles;

		// Guard clause: if roles is not an array, no overlap possible.
		if ( ! is_array( $user_roles ) ) {
			return false;
		}

		// For multisite, get the strictest role across all sites.
		if ( is_multisite() ) {
			$strict_roles = Rsssl_Two_Factor_Settings::get_strictest_role_across_sites( $user->ID, array( 'email' ) );

			// Array conversion for possible single-role.
			if ( is_string( $strict_roles ) ) {
				$strict_roles = ( '' === $strict_roles ) ? null : array( $strict_roles );
			}

			// No valid roles from multisite, no overlap possible.
			if ( empty( $strict_roles ) || ! is_array( $strict_roles ) ) {
				return false;
			}

			$user_roles = $strict_roles;
		}

		// Check if any of the user's roles are in the optional roles list.
		return ! empty( array_intersect( $user_roles, $optional_roles ) );
	}

	/**
	 * Set user status for two-factor authentication.
	 *
	 * @param int    $user_id User ID.
	 * @param string $status The status to set.
	 *
	 * @return void
	 */
	public static function set_user_status( int $user_id, string $status ): void {
		update_user_meta( $user_id, 'rsssl_two_fa_status_email', $status );
	}


	/**
	 * Returns the HTML for the selection option.
	 *
	 * @param WP_User $user The user object.
	 * @param  bool    $checked Whether the option is checked or not.
	 *
	 * @return void
	 * @throws Exception Throws an exception if the template file is not found.
	 */
	public static function get_selection_option( $user, bool $checked = false ): void {
		// Get the preferred method meta, which could be a string or an array.
		$preferred_method_meta = get_user_meta( $user->ID, 'rsssl_two_fa_set_provider', true );
		// Normalize the preferred method to always be an array.
		$preferred_methods = is_array( $preferred_method_meta ) ? $preferred_method_meta : (array) $preferred_method_meta;
		// Check if 'Rsssl_Two_Factor_Email' is the preferred method.
		$is_preferred      = in_array( 'Rsssl_Two_Factor_Email', $preferred_methods, true );
		$is_enabled        = (bool) get_user_meta( $user->ID, self::SECRET_META_KEY, true );
		$badge_class       = $is_enabled ? 'badge-enabled' : 'badge-default';
		$enabled_text      = $is_enabled ? esc_html__( 'Enabled', 'really-simple-ssl' ) : esc_html__( 'Disabled', 'really-simple-ssl' );
		$checked_attribute = $checked ? 'checked' : '';
		$title             = esc_html__( 'Email', 'really-simple-ssl' );
		$description       = esc_html__( 'Receive a code by email', 'really-simple-ssl' );

		// Check if any of the user's roles are in the forced roles list.
		$user_roles      = ! empty( $user->roles ) && is_array( $user->roles ) ? $user->roles : [];
		$forced_roles    = (array) rsssl_get_option( 'two_fa_forced_roles' );
		$is_forcible     = ! empty( array_intersect( $user_roles, $forced_roles ) );

		// Load the template.
		rsssl_load_template(
			'selectable-option.php',
			array(
				'badge_class'       => $badge_class,
				'enabled_text'      => $enabled_text,
				'checked_attribute' => $checked_attribute,
				'title'             => $title,
				'type'              => 'email', // Used this to identify the provider.
				'forcible'          => $is_forcible,
				'description'       => $description,
				'user'              => $user,
			),
			rsssl_path . 'assets/templates/two_fa'
		);
	}

	/**
	 * Check if a user is enabled based on their role.
	 *
	 * @param  WP_User $user  The user object to check.
	 *
	 * @return bool Whether the user is enabled or not.
	 */
	public static function is_enabled( WP_User $user ): bool {
        // todo - Do we need to check for a pro version here too?
		if ( ! $user->exists() ) {
			return false;
		}

        // Get the user roles.
        $user_roles = $user->roles;

        // Guard clause: if roles is not an array, no overlap possible.
        if ( ! is_array( $user_roles ) ) {
            return false;
        }

        // For multisite, get the strictest role across all sites.
        if ( is_multisite() ) {
            $strict_roles = Rsssl_Two_Factor_Settings::get_strictest_role_across_sites( $user->ID, array( 'email' ) );

            // Array conversion for possible single-role.
            if ( is_string( $strict_roles ) ) {
                $strict_roles = ( '' === $strict_roles ) ? null : array( $strict_roles );
            }

            // No valid roles from multisite, no overlap possible.
            if ( empty( $strict_roles ) || ! is_array( $strict_roles ) ) {
                return false;
            }

            $user_roles = $strict_roles;
        }

        // Get and normalize enabled roles.
        $enabled_roles = rsssl_get_option( 'two_fa_enabled_roles_email' );
        if ( ! is_array( $enabled_roles ) ) {
            $enabled_roles = array();
        }

        // Check if any user role is in the enabled roles list.
        return ! empty( array_intersect( $user_roles, $enabled_roles ) );
	}

    public static function is_configured( WP_User $user ): bool {
        $status = get_user_meta( $user->ID, 'rsssl_two_fa_status_email', true );
        return 'active' === $status;
    }

	public static function get_status( WP_User $user ): string {
		return Rsssl_Two_Factor_Settings::get_user_status( 'email', $user->ID );
	}

    public static function reset_meta_data(int $user_id): void
    {
        delete_user_meta( $user_id, self::RSSSL_TOKEN_META_KEY );
        delete_user_meta( $user_id, self::RSSSL_TOKEN_META_KEY_TIMESTAMP );
        delete_user_meta( $user_id, 'rsssl_two_fa_status_email' );
        delete_user_meta( $user_id, 'rsssl_two_fa_skip_token' );
        delete_user_meta( $user_id, '_rsssl_factor_email_token_timestamp' );
        delete_user_meta( $user_id, '_rsssl_factor_email_token' );
        delete_user_meta( $user_id, '_rsssl_two_factor_nonce' );
    }
}

