<?php

defined( 'ABSPATH' ) || exit;

use Masteriyo\Addons\TwoFactorAuthentication\Setting;
use Masteriyo\Emails\EmailHooks;
use Masteriyo\Roles;


if ( ! function_exists( 'masteriyo_is_2fa_required' ) ) {

	/**
	 * Determine if two-factor authentication (2FA) is required for a user.
	 *
	 * This function checks whether the given user is required to use 2FA
	 * based on the user's role. It provides several filters for extending
	 * or overriding its default behavior.
	 *
	 * @since 2.7.0
	 *
	 * @param WP_User $user User object.
	 *
	 * @return bool True if 2FA is required, false otherwise.
	 */
	function masteriyo_is_2fa_required( $user ) {
		$masteriyo_user = masteriyo_get_user( $user );

		if ( is_wp_error( $masteriyo_user ) || is_null( $masteriyo_user ) ) {
			return false;
		}

		$roles_2fa_enabled  = array( Roles::INSTRUCTOR, Roles::STUDENT );
		$roles_2fa_disabled = array( Roles::ADMIN );

		/**
		 * Filter to enforce 2FA for all roles.
		 *
		 * If this filter returns true, then the roles-specific checks are bypassed
		 * and 2FA is enforced for all roles.
		 *
		 * @since 2.7.0
		 *
		 * @param bool     $enforce_for_all Whether to enforce 2FA for all roles.
		 * @param WP_User  $user            User object.
		 */
		$enforce_for_all = apply_filters( 'masteriyo_enforce_2fa_for_all_roles', false, $user );

		if ( $enforce_for_all ) {
			return true;
		}

		/**
		 * Filters the roles for which 2FA is enabled.
		 *
		 * @since 2.7.0
		 *
		 * @param array    $roles_2fa_enabled Array of roles for which 2FA is enabled.
		 * @param WP_User  $user              User object.
		 */
		$roles_2fa_enabled = apply_filters( 'masteriyo_roles_2fa_enabled', $roles_2fa_enabled, $user );

		/**
		 * Filters the roles for which 2FA is disabled.
		 *
		 * @since 2.7.0
		 *
		 * @param array    $roles_2fa_disabled Array of roles for which 2FA is disabled.
		 * @param WP_User  $user               User object.
		 */
		$roles_2fa_disabled = apply_filters( 'masteriyo_roles_2fa_disabled', $roles_2fa_disabled, $user );

		$require_2fa = ! empty(
			array_filter(
				$roles_2fa_enabled,
				function( $role ) use ( $masteriyo_user ) {
					return $masteriyo_user->has_roles( $role );
				}
			)
		);

		$skip_2fa = ! empty(
			array_filter(
				$roles_2fa_disabled,
				function( $role ) use ( $masteriyo_user ) {
					return $masteriyo_user->has_roles( $role );
				}
			)
		);

		if ( $skip_2fa ) {
			return false;
		} elseif ( $require_2fa ) {
			return true;
		} else {
			return false;
		}

	}
}

if ( ! function_exists( 'masteriyo_is_request_from_wp_login' ) ) {

	/**
	 * Check is request from WP login screen.
	 *
	 * @since 2.7.0
	 *
	 * @return boolean
	 */
	function masteriyo_is_request_from_wp_login() {
		return isset( $_SERVER['REQUEST_URI'] ) && strpos( $_SERVER['REQUEST_URI'], 'wp-login.php' ) !== false;
	}
}

if ( ! function_exists( 'masteriyo_is_request_from_masteriyo_login' ) ) {

	/**
	 * Check is request from Masteriyo Login screen.
	 *
	 * @since 2.7.0
	 *
	 * @return boolean
	 */
	function masteriyo_is_request_from_masteriyo_login() {
		return isset( $_POST['action'] ) && 'masteriyo_login' === sanitize_text_field( $_POST['action'] ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
	}
}

if ( ! function_exists( 'masteriyo_generate_otp_code' ) ) {

	/**
	 * Generate OTP code.
	 *
	 * @since 2.7.0
	 *
	 * @param int $user_id ID of the user.
	 *
	 * @return string Generated OTP code.
	 */
	function masteriyo_generate_otp_code( $user_id ) {

		$code_length = masteriyo_get_login_otp_length();

		// Define characters that could be used in OTP code.
		$characters        = '0123456789';
		$characters_length = strlen( $characters );

		$random_code = '';

		// Generate OTP code.
		for ( $i = 0; $i < $code_length; $i++ ) {
			$random_code .= $characters[ \wp_rand( 0, $characters_length - 1 ) ];
		}

		// Set the user metas.
		update_user_meta( $user_id, 'masteriyo_tfa_otp', $random_code );
		update_user_meta( $user_id, 'masteriyo_tfa_otp_time', time() );

		$resend_count = get_user_meta( $user_id, 'masteriyo_tfa_otp_resend_count', true );

		if ( ! $resend_count ) {
			update_user_meta( $user_id, 'masteriyo_tfa_otp_resend_count', 0 );
		}

		return $random_code;
	}
}

if ( ! function_exists( 'masteriyo_get_login_otp_expiration_time' ) ) {

	/**
	 * Retrieve the OTP expiration time in human-readable format or seconds.
	 *
	 * This function fetches the OTP expiration setting and returns it either in seconds or
	 * in a human-readable format such as "1 hour 2 minutes".
	 *
	 * @since 2.7.0
	 *
	 * @param bool $format If true, returns time in human-readable format.
	 *
	 * @return string|int OTP expiration time.
	 */
	function masteriyo_get_login_otp_expiration_time( $format = false ) {
		$seconds = Setting::get( 'opt_expiration' );
		$seconds = absint( $seconds );

		// Fall back to 30 minutes if the expiration time is less than 2 minutes.
		if ( 120 > $seconds ) {
			$seconds = 30 * 60;
		}

		if ( $format ) {
			return masteriyo_seconds_to_human_readable( $seconds );
		}

		return $seconds;
	}
}

if ( ! function_exists( 'masteriyo_seconds_to_human_readable' ) ) {

	/**
	 * Convert a given number of seconds into a human-readable time format.
	 *
	 * This function takes an integer number of seconds and returns a human-readable string
	 * representing the time duration. For example, 3722 seconds would be converted to "1 hour 2 minutes 2 seconds".
	 *
	 * @since 2.7.0
	 * @param int $seconds The number of seconds to convert.
	 * @return string Human-readable time.
	 */
	function masteriyo_seconds_to_human_readable( $seconds ) {
		$hours   = floor( $seconds / 3600 );
		$minutes = floor( ( $seconds - ( $hours * 3600 ) ) / 60 );
		$seconds = $seconds % 60;

		$parts = array();
		if ( $hours > 0 ) {
			$parts[] = "$hours hour" . ( 1 === $hours ? '' : 's' );
		}
		if ( $minutes > 0 ) {
			$parts[] = "$minutes minute" . ( 1 === $minutes ? '' : 's' );
		}
		if ( $seconds > 0 ) {
			$parts[] = "$seconds second" . ( 1 === $seconds ? '' : 's' );
		}

		return implode( ' ', $parts );
	}
}


if ( ! function_exists( 'masteriyo_get_login_otp_length' ) ) {

	/**
	 * Retrieve the OTP length.
	 *
	 * @since 2.7.0
	 *
	 * @return int OTP length.
	 */
	function masteriyo_get_login_otp_length() {
		$length = Setting::get( 'opt_length' );
		$length = absint( $length );

		// Fall back to 6. if the length is less than 4 or greater than 10.
		$min_length = 4;
		$max_length = 10;

		if ( $length < $min_length || $length > $max_length ) {
			$length = 6;
		}

		return $length;
	}
}

if ( ! function_exists( 'masteriyo_login_otp_verification_handler' ) ) {
	/**
	 * Verify OTP during login.
	 *
	 * @since 2.7.0
	 *
	 * @param int    $user_id  User ID for the OTP verification.
	 * @param string $otp_code The OTP code to verify.
	 *
	 * @return bool  True if the OTP code is verified successfully, otherwise false.
	 */
	function masteriyo_login_otp_verification_handler( $user_id, $otp_code ) {
		$saved_otp       = get_user_meta( $user_id, 'masteriyo_tfa_otp', true );
		$saved_otp_time  = get_user_meta( $user_id, 'masteriyo_tfa_otp_time', true );
		$saved_otp_time  = absint( $saved_otp_time );
		$expiration_time = masteriyo_get_login_otp_expiration_time(); // In seconds.

		// Check OTP validity.
		if ( $otp_code === $saved_otp && ( time() - $saved_otp_time ) <= $expiration_time ) {
			// OTP is verified, clean up all related meta data.
			delete_user_meta( $user_id, 'masteriyo_tfa_otp' );
			delete_user_meta( $user_id, 'masteriyo_tfa_otp_time' );
			delete_user_meta( $user_id, 'masteriyo_tfa_otp_resend_count' );

			return true;
		}

		return false;
	}
}

if ( ! function_exists( 'masteriyo_can_resend_login_otp' ) ) {
	/**
	 * Check if OTP can be resent.
	 *
	 * @since 2.7.0
	 *
	 * @param int $user_id User ID for whom we are checking OTP resend ability.
	 *
	 * @return bool True if OTP can be resent, otherwise false.
	 */
	function masteriyo_can_resend_login_otp( $user_id ) {
		$current_resend_count = get_user_meta( $user_id, 'masteriyo_tfa_otp_resend_count', true );

		$max_resend_attempts = masteriyo_get_login_opt_resend_max_attempts();

		// Check if OTP can be resent.
		if ( absint( $current_resend_count ) < $max_resend_attempts ) {
			return true;
		}

		return false;
	}
}

if ( ! function_exists( 'masteriyo_get_login_opt_resend_max_attempts' ) ) {

	/**
	 * Retrieve the OTP resend max attempts.
	 *
	 * @since 2.7.0
	 *
	 * @return int OTP resend max attempts.
	 */
	function masteriyo_get_login_opt_resend_max_attempts() {
		$attempts = Setting::get( 'opt_resend_max_attempts' );
		$attempts = absint( $attempts );

		// Fall back to 10. if the length is less than 1 or greater than 100.
		$min_attempts = 1;
		$max_attempts = 100;

		if ( $attempts < $min_attempts || $attempts > $max_attempts ) {
			$attempts = 10;
		}

		return $attempts;
	}
}

if ( ! function_exists( 'masteriyo_get_login_opt_resend_interval' ) ) {

	/**
	 * Retrieve the OTP resend interval.
	 *
	 * @since 2.7.0
	 *
	 * @return int OTP resend interval.
	 */
	function masteriyo_get_login_opt_resend_interval() {
		$interval = Setting::get( 'opt_resend_interval' );
		$interval = absint( $interval );

		// Fall back to 2. if the length is less than 1.
		$min_interval = 1;

		if ( $interval < $min_interval ) {
			$interval = 2;
		}

		return $interval;
	}
}

if ( ! function_exists( 'masteriyo_do_login' ) ) {

	/**
	 * Perform user login by setting authentication cookie and triggering hooks.
	 *
	 * @since 2.7.0
	 *
	 * @param WP_User $user The WP_User object of the user being logged in.
	 *
	 * @return void
	 */
	function masteriyo_do_login( $user ) {
		// Set customer authentication cookie.
		masteriyo_set_customer_auth_cookie( $user->ID );

		/**
		 * Fires before the user is authenticated.
		 *
		 * @since 2.7.0
		 *
		 * The variables passed to the callbacks are blank, so this does not
		 * replicate the `wp_authenticate()` function.
		 *
		 * This provides a way for applications to allow/disallow a user
		 * without having to hack the `wp_authenticate` function.
		 */
		apply_filters( 'authenticate', $user, $user->user_login, '' );

		/**
		 * Fires after the user has successfully logged in.
		 *
		 * @since 2.7.0
		 *
		 * @param string  $user_login Username.
		 * @param WP_User $user       WP_User object of the logged-in user.
		 */
		do_action( 'wp_login', $user->user_login, $user );
	}
}

if ( ! function_exists( 'masteriyo_can_update_otp_resend_count' ) ) {
	/**
	 * Updates the OTP resend count if applicable.
	 *
	 * This function checks whether the OTP can be resent based on the configured interval
	 * and resend limit. If applicable, it updates the user meta data to keep track of resend counts.
	 *
	 * @since 2.7.0
	 *
	 * @param \Masteriyo\Models\User $user User for whom the OTP is being resent.
	 *
	 * @return bool|WP_Error Returns true if successful, or WP_Error if the OTP cannot be resent.
	 */
	function masteriyo_can_update_otp_resend_count( $user ) {

		$user_id         = $user->get_id();
		$current_time    = time();
		$saved_otp_time  = get_user_meta( $user_id, 'masteriyo_tfa_otp_time', true );
		$saved_otp_time  = absint( $saved_otp_time );
		$resend_count    = get_user_meta( $user_id, 'masteriyo_tfa_otp_resend_count', true );
		$resend_count    = absint( $resend_count );
		$resend_interval = masteriyo_get_login_opt_resend_interval(); // in seconds.

		if ( ! masteriyo_can_resend_login_otp( $user_id ) ) {
			return new WP_Error( 'otp_resend_limit_exceeded', 'You have reached the maximum number of OTP resend attempts.' );
		}

		// Check if enough time has passed before allowing OTP resend.
		if ( ( $current_time - $saved_otp_time ) < $resend_interval ) {
			return new WP_Error( 'otp_resend_time_interval_violation', 'Please wait for the required time interval before attempting to resend the OTP.' );
		}

		// Update resend count.
		update_user_meta( $user_id, 'masteriyo_tfa_otp_resend_count', ++$resend_count );

		return true;
	}
}

if ( ! function_exists( 'masteriyo_login_otp_resend_handler' ) ) {
	/**
	 * Handles the OTP resend based on preliminary checks and user roles.
	 *
	 * This function will first call masteriyo_can_update_otp_resend_count to perform
	 * preliminary checks. If true, it will proceed to schedule OTP emails based on
	 * the user roles.
	 *
	 * @since 2.7.0
	 *
	 * @param \Masteriyo\Models\User $user User for whom the OTP is being resent.
	 *
	 * @return int|WP_Error Returns the remaining OTP resend count if successful, or WP_Error if failed.
	 */
	function masteriyo_login_otp_resend_handler( $user ) {

		$result = masteriyo_can_update_otp_resend_count( $user );

		if ( true === $result ) {

			$user_id = $user->get_id();

			if ( ! ( $user->has_roles( Roles::STUDENT ) || $user->has_roles( Roles::INSTRUCTOR ) ) ) {
				return $result;
			}

			EmailHooks::schedule_login_otp_email( $user_id, $user );

			$resend_count     = get_user_meta( $user_id, 'masteriyo_tfa_otp_resend_count', true );
			$resend_count     = absint( $resend_count );
			$max_resend_count = masteriyo_get_login_opt_resend_max_attempts();

			return $max_resend_count - $resend_count;
		}

		return $result; // This will be a WP_Error object if masteriyo_can_update_otp_resend_count failed.
	}
}

if ( ! function_exists( 'masteriyo_get_resend_remaining_time' ) ) {

	/**
	 * Retrieve the remaining time for OTP resend.
	 *
	 * @since 2.7.0
	 *
	 * @param int $user_id User ID to fetch the saved OTP time.
	 *
	 * @return int Remaining time for OTP resend.
	 */
	function masteriyo_get_resend_remaining_time( $user_id ) {
		$saved_otp_time = get_user_meta( $user_id, 'masteriyo_tfa_otp_time', true );
		$saved_otp_time = absint( $saved_otp_time );

		$opt_resend_interval = masteriyo_get_login_opt_resend_interval();

		$current_time        = time();
		$resend_allowed_time = $saved_otp_time + $opt_resend_interval;

		$resend_remaining_time = $resend_allowed_time - $current_time;

		if ( $resend_remaining_time < 0 ) {
			$resend_remaining_time = 0;
		}

		return $resend_remaining_time;
	}
}

if ( ! function_exists( 'masteriyo_get_resend_remaining_count' ) ) {

	/**
	 * Retrieve the remaining count for OTP resend.
	 *
	 * @since 2.7.0
	 *
	 * @param int $user_id User ID to fetch the saved OTP resend count.
	 *
	 * @return int Remaining count for OTP resend.
	 */
	function masteriyo_get_resend_remaining_count( $user_id ) {
		$resend_count = get_user_meta( $user_id, 'masteriyo_tfa_otp_resend_count', true );
		$resend_count = absint( $resend_count );

		$max_resend_count       = masteriyo_get_login_opt_resend_max_attempts();
		$resend_count_remaining = $max_resend_count - $resend_count;

		if ( $resend_count_remaining < 0 ) {
			$resend_count_remaining = 0;
		}

		return $resend_count_remaining;
	}
}

if ( ! function_exists( 'masteriyo_get_user_email_by_id' ) ) {
	/**
	 * Retrieve the email address of a user by their User ID.
	 *
	 * @since 2.7.0
	 *
	 * @param int $user_id The User ID for which to fetch the email address.
	 *
	 * @return string The email address of the user or an empty string if the user does not exist or an error occurs.
	 */
	function masteriyo_get_user_email_by_id( $user_id ) {
		if ( ! is_int( $user_id ) || $user_id <= 0 ) {
			return '';
		}

		$user = get_user_by( 'ID', $user_id );

		if ( is_wp_error( $user ) || is_null( $user ) ) {
			return '';
		}

		return $user->user_email;
	}
}


/**
 * Generate a secure hash for OTP authentication.
 *
 * @since 2.17.2
 * @param int $user_id User ID.
 * @return string Hashed token.
 */
function masteriyo_generate_otp_hash( $user_id ) {
	$secret_key = 'masteriyo-otp-secret';
	$timestamp  = time();
	$token_raw  = $user_id . '|' . $timestamp;
	$token_hash = hash_hmac( 'sha256', $token_raw, $secret_key );

	update_user_meta( $user_id, '_otp_token', $token_hash );
	update_user_meta( $user_id, '_otp_token_expiry', $timestamp + 600 );

	return base64_encode( $token_raw . '|' . $token_hash ); //phpcs:ignore
}

/**
 * Verify OTP hash.
 *
 * @since 2.17.2
 * @param string $token Received token from the request.
 * @return int|false User ID if valid, false if invalid.
 */
function masteriyo_verify_otp_hash( $token ) {
	if ( empty( $token ) ) {
		return false;
	}

	$secret_key = 'masteriyo-otp-secret';
	$decoded    = base64_decode( $token ); //phpcs:ignore
	$parts      = explode( '|', $decoded );

	if ( count( $parts ) !== 3 ) {
		return false;
	}

	list( $user_id, $timestamp, $provided_hash ) = $parts;

	// Check expiration time
	$stored_expiry = get_user_meta( $user_id, '_otp_token_expiry', true );
	if ( time() > $stored_expiry ) {
		return false;
	}

	// Verify hash
	$expected_hash = hash_hmac( 'sha256', $user_id . '|' . $timestamp, $secret_key );
	if ( ! hash_equals( $expected_hash, $provided_hash ) ) {
		return false;
	}

	return absint( $user_id );
}
