<?php
/**
 * Holds the logic for the profile page.
 *
 * @package REALLY_SIMPLE_SSL
 */

namespace RSSSL\Security\WordPress\Two_Fa;

use Exception;
use RSSSL\Pro\Security\WordPress\Two_Fa\Providers\Rsssl_Two_Factor_Totp;
use RSSSL\Pro\Security\WordPress\Two_Fa\Rsssl_Two_Factor_Backup_Codes;
use RSSSL\Security\WordPress\Two_Fa\Providers\Rsssl_Provider_Loader;
use RSSSL\Security\WordPress\Two_Fa\Providers\Rsssl_Two_Factor_Email;
use RSSSL\Security\WordPress\Two_Fa\Traits\Rsssl_Two_Fa_Helper;
use WP_User;

if (!class_exists('Rsssl_Two_Factor_Profile_Settings')) {
    /**
     * Class Rsssl_Two_Factor_Profile_Settings
     *
     * This class is responsible for handling the Two-Factor Authentication settings on the user profile page.
     *
     * @package REALLY_SIMPLE_SSL
     */
    class Rsssl_Two_Factor_Profile_Settings
    {
	    use Rsssl_Two_Fa_Helper;

	    /**
	     * Instance of this class.
	     *
	     * @var Rsssl_Two_Factor_Profile_Settings
	     */
	    private static $instance = null;


	    /**
         * The available providers.
         *
         * @var array $available_providers An array to store the available providers.
         */
        private $available_providers = array();


        /**
         * The forced Two-Factor Authentication roles.
         *
         * @var array $forced_two_fa An array to store the forced Two-Factor Authentication roles.
         */
        private array $forced_two_fa = array();

	    /**
	     * Get instance of this class.
	     *
	     * @return Rsssl_Two_Factor_Profile_Settings
	     */
	    public static function get_instance() {
		    if (null === self::$instance) {
			    self::$instance = new self();
		    }
		    return self::$instance;
	    }

        /**
         * Constructor for the class.
         *
         * If the user is logged in, retrieve the user object and check if two-factor authentication is turned on for the user.
         * If two-factor authentication is enabled, add the necessary hooks.
         *
         * @return void
         */
	    private function __construct() {
		    if ( is_user_logged_in() ) {
			    $user_id = get_current_user_id();
			    $user    = get_user_by( 'ID', $user_id );
			    global $pagenow;

			    $relevant_ajax_actions = [ 'change_method_to_email', 'resend_email_code_profile' ];

			    if (
				    'profile.php' === $pagenow ||
				    ( 'user-edit.php' === $pagenow && isset( $_GET['user_id'] ) ) ||
				    ( defined( 'DOING_AJAX' ) && DOING_AJAX && isset( $_REQUEST['action'] ) && in_array( $_REQUEST['action'], $relevant_ajax_actions, true ) )
			    ) {
				    if ( $this->validate_two_turned_on_for_user( $user ) ) {
					    add_action( 'admin_init', array( $this, 'add_hooks' ) );
				    }
			    }
		    }
	    }

        /**
         * Add hooks for user profile page.
         *
         * This method adds hooks to display the Two-Factor Authentication settings on user profile pages.
         *
         * @return void
         */
        public function add_hooks(): void
        {
            if (is_user_logged_in()) {
                $errors = Rsssl_Parameter_Validation::get_cached_errors(get_current_user_id());
                if (!empty($errors)) {
                    // We display the errors.
                    foreach ($errors as $error) {
                        add_settings_error(
                            'two-factor-authentication',
                            'rsssl-two-factor-authentication-error',
                            $error['message'],
                            $error['type']
                        );
                    }
                }
            }
            add_action('show_user_profile', array($this, 'show_user_profile'));
            add_action('edit_user_profile', array($this, 'show_user_profile'));
            add_action('admin_enqueue_scripts', array($this, 'enqueue_scripts'));
            add_action('admin_enqueue_scripts', array($this, 'enqueue_styles'));
            add_action('personal_options_update', array($this, 'save_user_profile'));
            add_action('edit_user_profile_update', array($this, 'save_user_profile'));

	        add_action( 'wp_ajax_resend_email_code_profile', [$this, 'resend_email_code_profile_callback'] );
	        add_action( 'wp_ajax_change_method_to_email', [$this, 'start_email_validation_callback'] );

            if (isset($_GET['profile'], $_GET['_wpnonce']) && rest_sanitize_boolean(wp_unslash($_GET['profile']))) {
                self::set_active_provider(get_current_user_id(), 'email');
            }
        }

        /**
         * Resend the email code for the user.
         *
         * @return void
         */
        public function resend_email_code_profile_callback(): void
        {
            // Check for nonce (make sure your nonce name and action match what you output to the page)
            if ( ! isset( $_POST['login_nonce'] )
                || !wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['login_nonce'] ) ), 'update_user_two_fa_settings' ) ) {
                wp_send_json_error( array( 'message' => __( 'Invalid nonce.', 'really-simple-ssl' ) ), 403 );
            }

            // Ensure the user is logged in.
            if ( ! is_user_logged_in() ) {
                wp_send_json_error( array( 'message' => __( 'User not logged in.', 'really-simple-ssl' ) ), 401 );
            }

            // Get the user ID.
            $user_id = get_current_user_id();
            $user = get_user_by( 'ID', $user_id );
            Rsssl_Two_Factor_Email::get_instance()->generate_and_email_token($user, true);
            wp_send_json_success( array( 'message' => __('Verification code re-sent', 'really-simple-ssl') ), 200 );
        }

        /**
         * Starts the process of email validation for a user.
         *
         */
        public function start_email_validation_callback(): void
        {
            if(!is_user_logged_in()) {
                wp_send_json_error( array( 'message' => __( 'User not logged in.', 'really-simple-ssl' ) ), 401 );
            }
            $user = get_user_by('id', get_current_user_id());
            // Sending the email with the code.
            Rsssl_Two_Factor_Email::get_instance()->generate_and_email_token($user, true);
            $token = get_user_meta( $user->ID, Rsssl_Two_Factor_Email::RSSSL_TOKEN_META_KEY, true );
            wp_send_json_success( array( 'message' => __('Verification code sent', 'really-simple-ssl'), 'token' => $token ), 200 );
        }

        /**
         * Save the Two-Factor Authentication settings for the user.
         *
         * @param int $user_id The user ID.
         *
         * @noinspection UnusedFunctionResultInspection
         * @return void
         */
        public function save_user_profile(int $user_id): void
        {
            // We check if the user owns the profile.
            if (!current_user_can('edit_user', $user_id)) {
                return;
            }

            // Handle reset action
            if (isset($_POST['change_2fa_config_field'])) {
                if (
                    isset($_POST['reset_two_fa_nonce']) &&
                    wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['reset_two_fa_nonce'])), 'reset_two_fa_settings')
                ) {
                    $reset_input = filter_var($_POST['change_2fa_config_field'], FILTER_VALIDATE_BOOLEAN);
                    $this->maybe_the_user_resets_config($user_id, $reset_input);
                    add_settings_error(
                        'two-factor-authentication',
                        'rsssl-two-factor-authentication-reset',
                        __('Two-Factor Authentication settings have been reset.', 'really-simple-ssl'),
                        'updated'
                    );
                    // Redirect to avoid form resubmission
                    wp_redirect(add_query_arg('settings-updated', 'true'));
                    exit;
                }
                return;
            }

            if (isset($_POST['rsssl_two_fa_nonce']) && !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['rsssl_two_fa_nonce'])), 'update_user_two_fa_settings')) {
                return;
            }

            if (isset($_POST['change_2fa_config_field'])) {
                // We sanitize the input needs to be a boolean.
                $reset_input = filter_var($_POST['change_2fa_config_field'], FILTER_VALIDATE_BOOLEAN);
                $this->maybe_the_user_resets_config($user_id, $reset_input);
                return;
            }

            $params = new Rsssl_Parameter_Validation();
            $params::validate_user_id($user_id);
            $user = get_user_by('ID', $user_id);
            $params::validate_user($user);

            if (!isset($_POST['two-factor-authentication'])) {
                // reset the user's 2fa settings.
                // Delete all 2fa related user meta.
                Rsssl_Two_Fa_Status::delete_two_fa_meta($user->ID);
                // Set the rsssl_two_fa_last_login to now, so the user will be forced to use 2fa.
                update_user_meta($user->ID, 'rsssl_two_fa_last_login', gmdate('Y-m-d H:i:s'));
                // also make sure no lingering errpr messages are shown.
                Rsssl_Parameter_Validation::delete_cached_errors($user_id);
                return;
            }

            if (!isset($_POST['preferred_method'])) {
                return;
            }

            // now we check witch provider is selected from the $_POST.
            $params::validate_selected_provider($this->sanitize_method(sanitize_text_field(wp_unslash($_POST['preferred_method']))));
            $selected_provider = $this->sanitize_method(sanitize_text_field(wp_unslash($_POST['preferred_method'])));

            // if the selected provider is not then return.
            if (!$selected_provider) {
                return;
            }

            switch ($selected_provider) {
                case 'totp':
                    $current_status = Rsssl_Two_Factor_Settings::get_user_status('totp', $user_id);
//                    if ('active' === $current_status) {
//                        return;
//                    }
                    if ((empty($_POST['two-factor-totp-authcode']))
                        || !isset($_POST['two-factor-totp-key'])
                    ) {
                        add_settings_error(
                            'two-factor-authentication',
                            'rsssl-two-factor-authentication-error',
                            __('Two-Factor Authentication for TOTP failed. No Authentication code provided, please try again.', 'really-simple-ssl'),
                        );
                        $params::cache_errors($user_id);
                        return;
                    }

                    $params::validate_auth_code(sanitize_text_field(wp_unslash($_POST['two-factor-totp-authcode'])));
                    $params::validate_key(sanitize_text_field(wp_unslash($_POST['two-factor-totp-key'])));
                    $auth_code = sanitize_text_field(wp_unslash($_POST['two-factor-totp-authcode']));
                    $key = sanitize_text_field(wp_unslash($_POST['two-factor-totp-key']));

                    if (Rsssl_Two_Factor_Totp::setup_totp($user, $key, $auth_code)) {
                        self::set_active_provider($user_id, 'totp');
                        // We generate the backup codes.
                        Rsssl_Two_Factor_Backup_Codes::generate_codes(
                            $user,
                            array(
                                'cached' => true,
                            )
                        );
                    } else {
                        add_settings_error(
                            'two-factor-authentication',
                            'rsssl-two-factor-authentication-error',
                            __('The Two-Factor Authentication setup for TOTP failed. Please try again.', 'really-simple-ssl'),
                        );
                    }
                    // We cache the errors.
                    $params::cache_errors($user_id);
                    break;
                case 'email':
                    $current_status = Rsssl_Two_Factor_Settings::get_user_status('email', $user_id);
                    if ('active' === $current_status) {
                        return;
                    }
                    $user = get_user_by('ID', $user_id);
                    // fetch current status of the user for the email method.
                    $status = Rsssl_Two_Factor_Settings::get_user_status('email', $user->ID);
                    if ('active' === $status) {
                        return;
                    }
                    if (Rsssl_Two_Factor_Email::get_instance()->validate_authentication($user)) {
                        self::set_active_provider($user->ID, 'email');
                    } else {
                        add_settings_error(
                            'two-factor-authentication',
                            'rsssl-two-factor-authentication-error',
                            __('The Two-Factor Authentication setup for email failed. Please try again.', 'really-simple-ssl'),
                        );
                    }
                    break;
                case 'none':
                    // We disable the Two-Factor Authentication.
                    Rsssl_Two_Fa_Status::delete_two_fa_meta($user->ID);
                    break;
                default:
                    break;
            }

            $params::cache_errors($user_id);
        }

        /**
         * Sanitize the input method.
         *
         * @param string $method The input method.
         *
         * @return string The sanitized input method. Defaults to 'email' if not found in the allowed methods.
         */
        private function sanitize_method(string $method): string
        {
            $methods = array('totp', 'email', 'passkey', 'none');

            return in_array($method, $methods, true) ? sanitize_text_field($method) : 'email';
        }

        /**
         * Display the user profile with Two-Factor Authentication settings.
         *
         * @param WP_User $user The user object.
         *
         * @noinspection UnusedFunctionResultInspection
         * @return void
         * @throws Exception Throws an exception if the template file is not found.
         */
        public function show_user_profile(WP_User $user): void
        {
            // Check if the current user is viewing their own profile
            if ($user->ID !== get_current_user_id()) {
                return;
            }
            settings_errors('two-factor-authentication');
            settings_errors('rsssl-two-factor-authentication-error');

            $loader = Rsssl_Provider_Loader::get_loader();
            $available_providers = $loader::get_enabled_providers_for_user($user);
            $forced = !empty(array_intersect($user->roles, $this->forced_two_fa));
            $one_enabled = 'onboarding' !== Rsssl_Two_Factor_Settings::get_login_action($user->ID);
            $selected_provider = '';
            if ($one_enabled) {
                $selected_provider = strtolower(Rsssl_Two_Factor_Settings::get_configured_provider($user->ID));
            }
			$backup_codes = '';
			$key = '';
			$totp_url = '';

			/*
			 * Added this as a temporary fix to prevent errors when TOTP is not available.
			 * TODO: Make a better solution to handle the case when TOTP is not available.
			 */
			if (isset($available_providers['totp'])) {
				$backup_codes = Rsssl_Two_Factor_Settings::get_backup_codes( $user->ID );
				$key          = Rsssl_Two_Factor_Totp::generate_key();
				$totp_url     = Rsssl_Two_Factor_Totp::generate_qr_code_url( $user, $key );
			}

            wp_nonce_field('update_user_two_fa_settings', 'rsssl_two_fa_nonce');

            // Pass user_id instead of user object to prevent object corruption during template rendering
            $user_id = $user->ID;

            $data = array(
                'key' => $key,
                'totp_url' => $totp_url,
                'backup_codes' => $backup_codes,
                'selected_provider' => $selected_provider,
                'one_enabled' => $one_enabled,
                'forced' => $forced,
                'available_providers' => $available_providers,
                'user_id' => $user_id,
                'login_nonce' => wp_create_nonce('rsssl_login_nonce'),
            );
            $data = self::removeCircularReferences($data);
            $data_js = 'rsssl_profile.totp_data = ' . json_encode($data, JSON_THROW_ON_ERROR) . ';';

            $passkeys_enabled = rsssl_get_option('enable_passkey_login' );

            wp_add_inline_script('rsssl-profile-settings', $data_js);

            // We load the needed template for the Two-Factor Authentication settings.
            rsssl_load_template(
                'profile-settings.php',
                compact(
                    'user_id',
                    'available_providers',
                    'forced',
                    'one_enabled',
                    'selected_provider',
                    'backup_codes',
                    'totp_url',
                    'key',
                    'passkeys_enabled'
                ),
                rsssl_path . 'assets/templates/two_fa/'
            );
        }

        /**
         * Validates if the Two-Factor Authentication is turned on for the user.
         *
         * @param WP_User $user The user object.
         *
         * @return bool Returns true if Two-Factor Authentication is turned on for the user, false otherwise.
         */
        private function validate_two_turned_on_for_user(WP_User $user): bool
        {
            // Get the setting for the system to check if it is turned on.
            $enabled_two_fa = rsssl_get_option('login_protection_enabled');
            $providers = Rsssl_Provider_Loader::get_loader()::get_user_enabled_providers($user);
            $option = rsssl_get_option('two_fa_forced_roles');
            $this->forced_two_fa = $option !== false ? $option : array();

            return $enabled_two_fa && !empty($providers);
        }

        /**
         * Enqueues the RSSSL profile settings script.
         *
         * @return void
         */
        public function enqueue_scripts(): void
        {
            $path = trailingslashit(rsssl_url) . 'assets/features/two-fa/assets.min.js';
            $file_path = trailingslashit(rsssl_path) . 'assets/features/two-fa/assets.min.js';
            $backup_codes = Rsssl_Two_Factor_Settings::get_backup_codes(get_current_user_id());
            $user = get_user_by('ID', get_current_user_id());
            // We check if the backup codes are available.
            wp_register_script('rsssl-profile-settings', $path, array(), filemtime($file_path), true);
            wp_enqueue_script('rsssl-profile-settings');
            wp_localize_script('rsssl-profile-settings', 'rsssl_profile', array(
                'ajax_url'      => admin_url( 'admin-ajax.php' ),
                'backup_codes' => $backup_codes,
                'root' => esc_url_raw(rest_url(Rsssl_Two_Factor::REST_NAMESPACE)),
                'user_id' => get_current_user_id(),
                'origin' => 'profile',
                'redirect_to' => 'rsssl_no_redirect', //added this for comparison in the json output.
                'login_nonce' => Rsssl_Two_Fa_Authentication::create_login_nonce(get_current_user_id())['rsssl_key'],
                'user_name' => $user->display_name,
                'display_name' => $user->user_nicename . ' (' . $user->user_email . ')',
                'translatables' => apply_filters('rsssl_two_factor_translatables', []),
            ));
        }

        /**
         * Enqueues the RSSSL profile settings stylesheet.
         *
         * @return void
         */
        public function enqueue_styles(): void
        {
            $path = trailingslashit(rsssl_url) . 'assets/features/two-fa/styles.css';
            $file_path = trailingslashit(rsssl_path) . 'assets/features/two-fa/styles.css';
            wp_enqueue_style('rsssl-profile-style', $path, array(), filemtime($file_path));
        }

        /**
         * Checks if the user resets the configuration and actually reset everything.
         *
         * @param int $user_id The ID of the user.
         * @param $reset_input
         *
         * @return bool
         */
        private function maybe_the_user_resets_config(int $user_id, $reset_input): bool
        {
            // If the reset is true, we do the reset.
            if ($reset_input && $user_id) {
                // We reset the user's Two-Factor Authentication settings.
                Rsssl_Two_Fa_Status::delete_two_fa_meta($user_id);
            }

            return $reset_input;
        }

        /**
         * Remove circular references from the data.
         *
         * @param $data
         * @param array $seen
         * @return mixed|null
         */
        public static function removeCircularReferences(&$data, array &$seen = [])
        {
            if (is_array($data)
                || is_object($data)
            ) {
                if (in_array($data, $seen, true)) {
                    return null; // Circular reference detected, return null or handle appropriately
                }
                $seen[] = $data;
                foreach ($data as &$value) {
                    $value = self::removeCircularReferences($value, $seen);
                }
            }
            return $data;
        }
    }
}
