<?php

if (!defined('ABSPATH')) {
    die('You are not allowed to call this page directly.');
}

class MeprAccountCtrl extends MeprBaseCtrl
{
    /**
     * Tracks if the account nonce has already been verified in this request.
     *
     * @var boolean
     */
    private static $account_nonce_verified = false;

    /**
     * Loads hooks for account-related actions and filters.
     *
     * @return void
     */
    public function load_hooks()
    {
        add_action('wp_enqueue_scripts', [$this,  'enqueue_scripts']);
        add_action('init', [$this,  'maybe_update_username']); // Need to use init for cookie stuff and to get old and new emails.
        add_action('mepr_above_checkout_form', [$this,  'maybe_show_broken_sub_message']); // Show message on checkout form with link to update broken sub.
        add_action('wp_ajax_save_profile_changes', [$this, 'save_profile_fields']);
        add_action('wp_ajax_mepr_process_update_account_form', [$this, 'process_update_account_form_ajax']);


        /*
         * These are dependent on the the theme/template supporting them.
         * To Turn On:
            add_filter('mepr_account_nav_page_titles', 'mepr_cust_on_switch');
            add_filter('mepr_account_nav_broswer_titles', 'mepr_cust_on_switch');
            function mepr_cust_on_switch($on) {
            return true;
            }
         */
        add_filter('the_title', [$this, 'account_page_the_title'], 10, 2);
        add_filter('wp_title', [$this, 'account_page_browser_title']);

        // Shortcodes.
        MeprHooks::add_shortcode('mepr_account_form', [$this, 'account_form_shortcode']);
        MeprHooks::add_shortcode('mepr_account_link', [$this, 'get_account_links']);
        MeprHooks::add_shortcode('mepr_account_info', [$this, 'output_account_meta']);
        MeprHooks::add_shortcode('mepr_offline_instructions', [$this, 'offline_gateway_instructions']); // Offline gateway instructions shortcode.
    }

    // What if a user has a failed payment on a subscription, and instead of updating their CC
    // They accidentally go to purchase a new subscription instead? This could lead to a double billing
    // So let's warn them here.
    /**
     * Shows a warning message when a user with a broken subscription attempts to purchase a new one.
     *
     * @param integer $prd_id The product ID.
     *
     * @return void
     */
    public function maybe_show_broken_sub_message($prd_id)
    {
        global $pagenow;

        if ($pagenow === 'post.php') {
            return;
        }

        $mepr_options = MeprOptions::fetch();
        $user         = MeprUtils::get_currentuserinfo();
        $errors       = [];

        if ($user !== false) {
            $enabled_prd_ids = $user->get_enabled_product_ids($prd_id);

            if (!empty($enabled_prd_ids)) { // If it's not empty, then the user already has an Enabled subscription for this membership.
                $prd = new MeprProduct($prd_id);

                $show_broken_sub_message = apply_filters_deprecated(
                    'maybe_show_broken_sub_message_override',
                    [true, $prd],
                    '1.12.12',
                    'mepr_maybe_show_broken_sub_message'
                );

                $show_broken_sub_message = MeprHooks::apply_filters('mepr_maybe_show_broken_sub_message', $show_broken_sub_message, $prd);

                if (!$prd->simultaneous_subscriptions && $show_broken_sub_message) {
                    $errors[] = sprintf(
                        // Translators: %1$s: opening anchor tag, %2$s: closing anchor tag.
                        _x('You already have a subscription to this Membership. Please %1$supdate your payment details%2$s on the existing subscription instead of purchasing again.', 'ui', 'memberpress'),
                        '<a href="' . $mepr_options->account_page_url('action=subscriptions') . '">',
                        '</a>'
                    );
                    MeprView::render('/shared/errors', get_defined_vars());
                    ?>
          <!-- Hidden signup form -->
          <script>
            jQuery(document).ready(function($) {
              $('input[value="' + <?php echo (int) $prd_id; ?> +'"]').closest('form').hide();
            });
          </script>
                    <?php
                }
            }
        }
    }

    /**
     * Updates the username if it's an email address and the email is changing.
     *
     * @return void
     */
    public function maybe_update_username()
    {
        if (
            MeprUtils::is_post_request() &&
            isset($_POST['mepr-process-account']) && $_POST['mepr-process-account'] === 'Y' &&
            isset($_POST['mepr_account_nonce']) && wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['mepr_account_nonce'])), 'update_account')
        ) {
            // Remember that we've successfully verified the form nonce here,
            // before any auth cookie/session state is touched. `home()` can
            // then skip its own `check_admin_referer()` call which would
            // otherwise re-verify the same nonce against a session token
            // that may have been rotated by maybe_update_username_to_email().
            self::$account_nonce_verified = true;

            $mepr_user = MeprUtils::get_currentuserinfo();
            $new_email = sanitize_email(wp_unslash($_POST['user_email'] ?? ''));

            if ($mepr_user !== false && $mepr_user->user_email !== $new_email) {
                $this->maybe_update_username_to_email($mepr_user, $new_email);
            }
        }
    }

    /**
     * Updates the user's username to match their new email address when applicable.
     *
     * This only applies when the current username is an email that matches the old email,
     * and no other user already has the new email as their username.
     *
     * @param MeprUser $mepr_user The user whose username may need updating.
     * @param string   $new_email The new email address.
     *
     * @return void
     */
    private function maybe_update_username_to_email($mepr_user, $new_email)
    {
        global $wpdb;
        $mepr_options = MeprOptions::fetch();
        $old_email    = $mepr_user->user_email;

        if (
            $mepr_options->username_is_email && // Make sure setting is enabled.
            is_email($new_email) && // Make sure this isn't sql injected or something.
            is_email($mepr_user->user_login) && // Make sure we're not overriding a non-email username.
            $old_email === $mepr_user->user_login && // Make sure old email and old username match up.
            $old_email !== $new_email &&
            ! username_exists($new_email) // Make sure no one else has this email as their username.
        ) {
            // Build the update payload. When display_name still matches the old user_login
            // (WordPress' default at registration when no first/last name is set) or is an email, bump it
            // too so places that fall back to display_name (e.g. the ReadyLaunch header
            // avatar block) don't keep rendering the old email after the change.
            $update_display_name = (isset($mepr_user->display_name) && ($mepr_user->display_name === $mepr_user->user_email || $mepr_user->display_name === $mepr_user->user_login));
            $data                = ['user_login' => $new_email];
            $formats             = ['%s'];

            if ($update_display_name) {
                $data['display_name'] = $new_email;
                $formats[]            = '%s';
            }

            // Some trickery here to keep the user logged in.
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery
            $wpdb->update(
                $wpdb->users,
                $data,
                ['ID' => $mepr_user->ID],
                $formats,
                ['%d']
            );

            // Keep the caller's in-memory MeprUser in sync with what we just
            // wrote. Without this, the subsequent `$mepr_user->store()` call
            // in the caller (which goes through wp_update_user() and
            // array_merges the in-memory record over the fresh DB row) will
            // This silently reverts display_name back to the old value — leaving
            // the ReadyLaunch header rendering the old email forever.
            // The user_login field is not touched by wp_update_user(), but we mirror
            // it here anyway for consistency.
            if ($mepr_user instanceof MeprUser) {
                $mepr_user->user_login = $new_email;
                if ($update_display_name) {
                    $mepr_user->display_name = $new_email;
                }
            }

            clean_user_cache($mepr_user->ID); // Get rid of the user cache.
            wp_clear_auth_cookie(); // Clear their old cookie.
            wp_set_current_user($mepr_user->ID); // Set the current user again.
            wp_set_auth_cookie($mepr_user->ID, true, false); // Log the user back in w/out knowing their password.
            update_user_caches(new WP_User($mepr_user->ID));
        }
    }

    /**
     * Enqueues scripts and styles for the account page.
     *
     * @param boolean $force Whether to force enqueueing scripts.
     *
     * @return void
     */
    public function enqueue_scripts($force = false)
    {
        global $post;
        $mepr_options = MeprOptions::fetch();

        if ($force || MeprUser::is_account_page($post)) {
            $popup_ctrl = new MeprPopupCtrl();

            $has_phone = false;

            if (! empty($mepr_options->custom_fields)) {
                foreach ($mepr_options->custom_fields as $field) {
                    if ('tel' === $field->field_type && $field->show_in_account) {
                        $has_phone = true;
                        break;
                    }
                }
            }

            wp_register_style('jquery-magnific-popup', $popup_ctrl->popup_css);
            wp_enqueue_style('mp-account', MEPR_CSS_URL . '/account.css', ['jquery-magnific-popup'], MEPR_VERSION);

            wp_register_script('jquery-magnific-popup', $popup_ctrl->popup_js, ['jquery']);
            wp_enqueue_script('mp-account', MEPR_JS_URL . '/account.js', ['jquery','jquery-magnific-popup'], MEPR_VERSION);

            $pms = $mepr_options->payment_methods();

            if ($pms) {
                wp_register_script('mepr-checkout-js', MEPR_JS_URL . '/checkout.js', ['jquery', 'jquery.payment'], MEPR_VERSION);
                wp_register_script('mepr-default-gateway-checkout-js', MEPR_JS_URL . '/gateway/checkout.js', ['mepr-checkout-js'], MEPR_VERSION);
                foreach ($pms as $pm) {
                    if ($pm instanceof MeprBaseRealGateway) {
                        $pm->enqueue_user_account_scripts();
                    }
                }
            }

            // Check if there's a phone field.
            if ($has_phone) {
                wp_enqueue_style('mepr-phone-css', MEPR_CSS_URL . '/vendor/intlTelInput.min.css', '', '16.0.0');
                wp_enqueue_style('mepr-tel-config-css', MEPR_CSS_URL . '/tel_input.css', '', MEPR_VERSION);
                wp_enqueue_script('mepr-phone-js', MEPR_JS_URL . '/vendor/intlTelInput.js', '', '16.0.0', true);
                wp_enqueue_script('mepr-tel-config-js', MEPR_JS_URL . '/tel_input.js', ['mepr-phone-js', 'mp-account'], MEPR_VERSION, true);
                wp_localize_script('mepr-tel-config-js', 'meprTel', MeprHooks::apply_filters('mepr_phone_input_config', [
                    'defaultCountry' => strtolower(get_option('mepr_biz_country')),
                    'utilsUrl'       => MEPR_JS_URL . '/vendor/intlTelInputUtils.js',
                    'onlyCountries'  => '',
                    'i18n'           => [
                        'selectCountryCode'  => esc_html__('Select country code', 'memberpress'),
                        'countryCodeOptions' => esc_html__('Country code options', 'memberpress'),
                        'countryChangedTo'   => esc_html__('Country changed to', 'memberpress'),
                        'countryCode'        => esc_html__('country code', 'memberpress'),
                        'phoneNumberInput'   => esc_html__('Phone number input', 'memberpress'),
                    ],
                ]));
            }
        }
    }

    /**
     * Renders the account page based on the provided attributes.
     *
     * @param array $atts Attributes for rendering the account page.
     *
     * @return void
     */
    public function render($atts = [])
    {
        global $post;

        $mepr_current_user = MeprUtils::get_currentuserinfo();
        $expired_subs      = $mepr_current_user->subscription_expirations('expired', true);
        $mepr_options      = MeprOptions::fetch();

        // When this option is empty, the "Plain" permalink structure is in use.
        $account_url = MeprUtils::get_account_url();

        $delim = MeprAppCtrl::get_param_delimiter_char($account_url);

        if (MeprReadyLaunchCtrl::template_enabled('account') || MeprAppHelper::has_block('memberpress/pro-account-tabs') || MeprHooks::apply_filters('mepr_pro_templates_has_account_block', false)) {
            MeprView::render('/readylaunch/account/nav', get_defined_vars());
        } else {
            MeprView::render('/account/nav', get_defined_vars());
        }

        $action = MeprHooks::apply_filters('mepr_account_action', (isset($_REQUEST['action'])) ? sanitize_text_field(wp_unslash($_REQUEST['action'])) : false);


        $this->dispatch_account_action($action, $atts);
    }

    /**
     * Returns just the account nav HTML for use by ReadyLaunch blocks.
     *
     * @param array $atts Attributes (layout, nav_style).
     *
     * @return string
     */
    public function render_nav_html($atts = [])
    {
        $account_url          = MeprUtils::get_account_url();
        $delim                = MeprAppCtrl::get_param_delimiter_char($account_url);
        $layout               = isset($atts['layout']) ? $atts['layout'] : 'vertical';
        $nav_style            = isset($atts['nav_style']) ? $atts['nav_style'] : '';
        $omit_content_wrapper = true;

        return MeprView::get_string('/readylaunch/account/nav', get_defined_vars());
    }

    /**
     * Returns just the account content HTML for use by ReadyLaunch blocks.
     *
     * @param array $atts Attributes (show_welcome_image, welcome_image).
     *
     * @return string
     */
    public function render_content_html($atts = [])
    {
        add_filter('mepr_pro_templates_has_account_block', '__return_true');

        ob_start();

        $action = MeprHooks::apply_filters(
            'mepr_account_action',
            isset($_REQUEST['action']) ? sanitize_text_field(wp_unslash($_REQUEST['action'])) : false
        );

        $this->dispatch_account_action($action, $atts);

        return ob_get_clean();
    }

    /**
     * Dispatch an account-page action to its handler.
     *
     * Shared by {@see self::render()} (which echoes directly) and
     * {@see self::render_content_html()} (which buffers the output). Each handler echoes
     * its own markup, so capturing the output is the caller's responsibility.
     *
     * @param  string|false $action The resolved account action.
     * @param  array        $atts   Attributes passed through to the action handlers.
     * @return void
     */
    private function dispatch_account_action($action, $atts = []): void
    {
        switch ($action) {
            case 'payments':
                $this->payments();
                break;
            case 'subscriptions':
                $this->subscriptions();
                break;
            case 'newpassword':
                $this->password();
                break;
            case 'cancel':
                $this->cancel();
                break;
            case 'suspend':
                $this->suspend();
                break;
            case 'resume':
                $this->resume();
                break;
            case 'update':
                $this->update();
                break;
            case 'upgrade':
                $this->upgrade();
                break;
            default:
                // Allows you to override the content for a nav tab.
                ob_start();
                MeprHooks::do_action('mepr_account_nav_content', $action, $atts);
                $custom_content = ob_get_clean();

                if (empty($custom_content)) {
                    $this->home($atts);
                } else {
                    echo '<div class="mepr-' . esc_attr($action) . '-wrapper">' . $custom_content . '</div>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
                }
        }

        MeprHooks::do_action('mepr_after_account_render');
    }

    /**
     * Renders the home section of the account page.
     *
     * @param array $atts Attributes for rendering the home section.
     *
     * @return void
     */
    public function home($atts = [])
    {
        $mepr_current_user = MeprUtils::get_currentuserinfo();
        $mepr_options      = MeprOptions::fetch();
        $account_url       = $mepr_options->account_page_url();
        $delim             = MeprAppCtrl::get_param_delimiter_char($account_url);
        $errors            = [];
        $saved             = false;

        // Process welcome message: format content first, then sanitize
        // This ensures shortcodes, embeds, and blocks work correctly before sanitization.
        $welcome_message = stripslashes($mepr_options->custom_message);
        $welcome_message = MeprUtils::format_content($welcome_message); // Handles blocks, wpautop, shortcodes.

        // Autoembed any videos/URLs in the message (YouTube, Vimeo, etc.).
        if (class_exists('WP_Embed')) {
            $embed           = new WP_Embed();
            $welcome_message = $embed->autoembed($welcome_message);
        }

        $welcome_message = MeprAppHelper::wp_kses($welcome_message); // Sanitize with media support.

        if (
            MeprUtils::is_post_request() &&
            isset($_POST['mepr-process-account']) && $_POST['mepr-process-account'] === 'Y'
        ) {
            // The maybe_update_username() callback (hooked to init) may have already verified this
            // exact nonce before rotating the auth cookie. Re-verifying here after
            // a cookie rotation would fail because the nonce is signed with the
            // session token (GH #3489 — "link you followed has expired" wp_die).
            // Only run check_admin_referer() when it hasn't been verified yet.
            if (! self::$account_nonce_verified) {
                check_admin_referer('update_account', 'mepr_account_nonce');
            }
            $errors = MeprUsersCtrl::validate_extra_profile_fields(null, null, $mepr_current_user);
            $errors = MeprUser::validate_account($_POST, $errors);
            $errors = MeprHooks::apply_filters('mepr_validate_account', $errors, $mepr_current_user);

            if (empty($errors)) {
                // Need to find a better way to do this eventually but for now update the user's email.
                $new_email = sanitize_email(wp_unslash($_POST['user_email'] ?? ''));

                if ($mepr_current_user->user_email !== $new_email) {
                    $mepr_current_user->user_email = $new_email;
                    $mepr_current_user->store();
                    MeprHooks::do_action('mepr_update_new_user_email', $mepr_current_user);
                }

                // Save the usermeta.
                $saved = MeprUsersCtrl::save_extra_profile_fields($mepr_current_user->ID, true);

                // Reload the user now that we've updated it's info.
                $mepr_current_user = new MeprUser($mepr_current_user->ID);

                MeprHooks::do_action('mepr_save_account', $mepr_current_user);
                // Do not call mepr-account-updated here - it's already called in save_extra_profile_fields() above
                // MeprEvent::record('member-account-updated', $mepr_current_user);.
                if ($saved) {
                    MeprUtils::wp_redirect($account_url . $delim . 'action=home&message=account_saved');
                }
            }
        } elseif (isset($_REQUEST['message']) && $_REQUEST['message'] === 'password_updated') {
            $message = __('Your password was successfully updated.', 'memberpress');
        } elseif (isset($_REQUEST['message']) && $_REQUEST['message'] === 'account_saved') {
            $message = __('Your account has been saved.', 'memberpress');
        }

        // Load user last in case we saved above, we want the saved info to show up!
        $mepr_current_user = new MeprUser($mepr_current_user->ID);

        if (( MeprReadyLaunchCtrl::template_enabled('account') || MeprAppHelper::has_block('memberpress/pro-account-tabs') || MeprHooks::apply_filters('mepr_pro_templates_has_account_block', false) )) {
            if (is_array($atts)) {
                extract($atts, EXTR_SKIP);
            }
            $mepr_options = MeprOptions::fetch();
            $account_url  = $mepr_options->account_page_url();
            $delim        = MeprAppCtrl::get_param_delimiter_char($account_url);

            $mepr_current_user = MeprUtils::get_currentuserinfo();

            $address_fields = MeprUsersHelper::get_address_fields($mepr_current_user);
            $address_values = [];
            foreach ($address_fields as $address_field) {
                $value            = $mepr_current_user ? get_user_meta($mepr_current_user->ID, $address_field->field_key, true) : '';
                $address_values[] = $value;
            }

            static $unique_suffix = 0;
            ++$unique_suffix;

            $first_name_value = '';
            if (isset($user_first_name)) {
                $first_name_value = esc_attr(stripslashes($user_first_name));
            } elseif (MeprUtils::is_user_logged_in()) {
                $first_name_value = (string) $mepr_current_user->first_name;
            }

            $last_name_value = '';
            if (isset($user_last_name)) {
                $last_name_value = esc_attr(stripslashes($user_last_name));
            } elseif (MeprUtils::is_user_logged_in()) {
                $last_name_value = (string) $mepr_current_user->last_name;
            }

            $custom_fields = MeprUsersHelper::get_custom_fields();
            ob_start();
            foreach ($custom_fields as $custom_field) {
                if (isset($custom_field->show_in_account) && ! $custom_field->show_in_account) {
                    continue;
                }
                MeprUsersHelper::render_pro_templates_custom_field_values($custom_field, $mepr_current_user);
            }
            $custom_fields_values = ob_get_clean();

            $has_welcome_image = $welcome_image = null;

            // Has welcome image? Priority given to shortcode.
            if (isset($atts['show_welcome_image'])) {
                $show_welcome_image = filter_var($atts['show_welcome_image'], FILTER_VALIDATE_BOOLEAN);
            } elseif (isset($mepr_options->design_account_welcome_img)) {
                $show_welcome_image = $mepr_options->design_account_welcome_img;
            }

            // Get welcome image? Priority given to shortcode.
            if (isset($atts['welcome_image']) && $atts['welcome_image'] > 0) {
                $welcome_image = wp_get_attachment_url($atts['welcome_image']);
            } elseif (isset($mepr_options->design_account_welcome_img)) {
                $welcome_image = wp_get_attachment_url($mepr_options->design_account_welcome_img);
            }

            // Change the account url if it's a block template.
            if (MeprAppHelper::has_block('memberpress/pro-account-tabs')) {
                $account_url = get_permalink();
            }

            MeprView::render('/readylaunch/account/home', get_defined_vars());
        } else {
            MeprView::render('/account/home', get_defined_vars());
        }
    }

    /**
     * Renders the password update section of the account page.
     *
     * @return void
     */
    public function password()
    {
        $mepr_current_user = MeprUtils::get_currentuserinfo();
        $mepr_options      = MeprOptions::fetch();
        $account_url       = $mepr_options->account_page_url();
        $delim             = MeprAppCtrl::get_param_delimiter_char($account_url);

        if (isset($_REQUEST['error'])) {
            if ($_REQUEST['error'] === 'weak') {
                $errors = [__('Password update failed, please check that your password meets the minimum strength requirement.', 'memberpress')];
            } else {
                $errors = [__('Password update failed, please be sure your passwords match and try again.', 'memberpress')];
            }
        }

        if (MeprReadyLaunchCtrl::template_enabled('account')) {
            MeprView::render('/readylaunch/account/password', get_defined_vars());
        } else {
            MeprView::render('/account/password', get_defined_vars());
        }
    }

    /**
     * Renders the payments section of the account page.
     *
     * @param array $args Optional arguments for rendering payments.
     *
     * @return void
     */
    public function payments($args = [])
    {
        global $wpdb;
        $mepr_current_user = MeprUtils::get_currentuserinfo();
        $mepr_options      = MeprOptions::fetch();
        $account_url       = esc_url_raw(wp_unslash($_SERVER['REQUEST_URI'] ?? '')); // Use URI for BuddyPress compatibility.
        $delim             = MeprAppCtrl::get_param_delimiter_char($account_url);
        $perpage           = MeprHooks::apply_filters('mepr_payments_per_page', 10);
        $curr_page         = (isset($_GET['currpage']) && is_numeric($_GET['currpage'])) ? max(1, intval(wp_unslash($_GET['currpage']))) : 1;
        $start             = ($curr_page - 1) * $perpage;
        $end               = $start + $perpage;

        if (isset($args['mode']) && 'readylaunch' === $args['mode']) {
            $perpage = isset($args['count']) ? $args['count'] + $perpage : $perpage;
        }

        $list_table = MeprTransaction::list_table(
            'created_at',
            'DESC',
            $curr_page,
            '',
            'any',
            $perpage,
            [
                'member'   => $mepr_current_user->user_login,
                'statuses' => [MeprTransaction::$complete_str],
            ]
        );

        $payments  = $list_table['results'];
        $all       = $list_table['count'];
        $next_page = (($curr_page * $perpage) >= $all) ? false : $curr_page + 1;
        $prev_page = ($curr_page > 1) ? $curr_page - 1 : false;

        if ($mepr_options->design_enable_account_template) {
            MeprView::render('/readylaunch/account/payments', get_defined_vars());
        } else {
            MeprView::render('/account/payments', get_defined_vars());
        }
    }

    /**
     * Renders the subscriptions section of the account page.
     *
     * @param string $message Optional message to display.
     * @param array  $errors  Optional errors to display.
     * @param array  $args    Optional arguments for rendering subscriptions.
     *
     * @return void
     */
    public function subscriptions($message = '', $errors = [], $args = [])
    {
        global $wpdb;
        $mepr_current_user = MeprUtils::get_currentuserinfo();
        $mepr_options      = MeprOptions::fetch();
        $account_url       = esc_url_raw(wp_unslash($_SERVER['REQUEST_URI'] ?? '')); // Use URI for BuddyPress compatibility.
        $delim             = MeprAppCtrl::get_param_delimiter_char($account_url);
        $perpage           = MeprHooks::apply_filters('mepr_subscriptions_per_page', 10);
        $curr_page         = (isset($_GET['currpage']) && is_numeric($_GET['currpage'])) ? max(1, intval(wp_unslash($_GET['currpage']))) : 1;
        $start             = ($curr_page - 1) * $perpage;
        $end               = $start + $perpage;

        // This is necessary to optimize the queries ... only query what we need.
        $sub_cols = ['id','user_id','product_id','subscr_id','status','created_at','expires_at','active'];

        if (isset($args['mode']) && 'readylaunch' === $args['mode']) {
            $perpage = isset($args['count']) ? $args['count'] + $perpage : $perpage;
        }

        $table = MeprSubscription::account_subscr_table(
            'created_at',
            'DESC',
            $curr_page,
            '',
            'any',
            $perpage,
            false,
            [
                'member'   => $mepr_current_user->user_login,
                'statuses' => [
                    MeprSubscription::$active_str,
                    MeprSubscription::$suspended_str,
                    MeprSubscription::$cancelled_str,
                ],
            ],
            $sub_cols
        );

        $subscriptions = $table['results'];
        $all           = $table['count'];
        $next_page     = (($curr_page * $perpage) >= $all) ? false : $curr_page + 1;
        $prev_page     = ($curr_page > 1) ? $curr_page - 1 : false;

        if ($mepr_options->design_enable_account_template) {
            MeprView::render('/readylaunch/shared/errors', get_defined_vars());
            MeprView::render('/readylaunch/account/subscriptions', get_defined_vars());
        } else {
            MeprView::render('/shared/errors', get_defined_vars());
            MeprView::render('/account/subscriptions', get_defined_vars());
        }
    }

    /**
     * Suspends a user's subscription.
     *
     * @return void
     */
    public function suspend()
    {
        $this->verify_sub_action_nonce('mepr_suspend_sub_');

        $mepr_current_user = MeprUtils::get_currentuserinfo();
        $sub               = new MeprSubscription(intval(wp_unslash($_GET['sub'] ?? 0)));
        $errors            = [];
        $message           = '';

        if ((int) $sub->user_id === $mepr_current_user->ID) {
            $pm = $sub->payment_method();

            if ($pm->can('suspend-subscriptions')) {
                try {
                    $pm->process_suspend_subscription($sub->id);
                    $message = __('Your subscription was successfully paused.', 'memberpress');
                } catch (Exception $e) {
                    $errors[] = $e->getMessage();
                }
            }
        }

        $this->subscriptions($message, $errors);
    }

    /**
     * Resumes a user's subscription.
     *
     * @return void
     */
    public function resume()
    {
        $this->verify_sub_action_nonce('mepr_resume_sub_');

        $mepr_current_user = MeprUtils::get_currentuserinfo();
        $sub               = new MeprSubscription(intval(wp_unslash($_GET['sub'] ?? 0)));
        $errors            = [];
        $message           = '';

        if ((int) $sub->user_id === $mepr_current_user->ID) {
            $pm = $sub->payment_method();

            if ($pm->can('suspend-subscriptions')) {
                try {
                    $pm->process_resume_subscription($sub->id);
                    $message = __('You successfully resumed your subscription.', 'memberpress');
                } catch (Exception $e) {
                    $errors[] = $e->getMessage();
                }
            }
        }

        $this->subscriptions($message, $errors);
    }

    /**
     * Cancels a user's subscription.
     *
     * @return void
     */
    public function cancel()
    {
        $this->verify_sub_action_nonce('mepr_cancel_sub_');

        $mepr_current_user = MeprUtils::get_currentuserinfo();
        $sub               = new MeprSubscription(intval(wp_unslash($_GET['sub'] ?? 0)));
        $errors            = [];
        $message           = '';
        $success_message   = __('Your subscription was successfully cancelled.', 'memberpress');

        static $already_cancelled;

        if ($already_cancelled === true) {
            $message = $success_message;
        } elseif ((int) $sub->user_id === $mepr_current_user->ID) {
            $already_cancelled = true;
            $pm                = $sub->payment_method();

            if ($pm->can('cancel-subscriptions')) {
                try {
                    $pm->process_cancel_subscription($sub->id);
                    $message = $success_message;
                } catch (Exception $e) {
                    $errors[] = $e->getMessage();
                }
            }
        }

        $this->subscriptions($message, $errors);
    }

    /**
     * Verifies the CSRF nonce for a subscription management action (cancel/suspend/resume).
     *
     * The account-page links carry a per-subscription nonce created in
     * MeprBaseGateway::print_user_account_subscription_row_actions(). Without this
     * check a malicious page could force a logged-in member to cancel, suspend or
     * resume their own subscriptions via a simple forged GET request.
     *
     * @param  string $action_prefix Nonce action prefix (e.g. 'mepr_cancel_sub_').
     * @return void
     */
    private function verify_sub_action_nonce($action_prefix): void
    {
        $sub_id = intval(wp_unslash($_GET['sub'] ?? 0));
        $nonce  = isset($_REQUEST['_wpnonce']) ? sanitize_text_field(wp_unslash($_REQUEST['_wpnonce'])) : '';

        if (!wp_verify_nonce($nonce, $action_prefix . $sub_id)) {
            wp_die(esc_html__('Security check failed.', 'memberpress'));
        }
    }

    /**
     * Updates a user's subscription information.
     *
     * @return void
     */
    public function update()
    {
        $mepr_current_user = MeprUtils::get_currentuserinfo();
        $sub               = new MeprSubscription(intval(wp_unslash($_REQUEST['sub'] ?? 0)));

        if ((int) $sub->user_id === $mepr_current_user->ID) {
            $pm             = $sub->payment_method();
            $request_method = strtoupper(sanitize_text_field(wp_unslash($_SERVER['REQUEST_METHOD'] ?? '')));

            if ($request_method === 'GET') { // DISPLAY FORM.
                $pm->display_update_account_form($sub->id, []);
            } elseif ($request_method === 'POST') { // PROCESS FORM.
                $errors  = $pm->validate_update_account_form([]);
                $message = '';

                if (empty($errors)) {
                    try {
                        $pm->process_update_account_form($sub->id);
                        $message = __('Your account information was successfully updated.', 'memberpress');
                    } catch (Exception $e) {
                        $errors[] = $e->getMessage();
                    }
                }

                $pm->display_update_account_form($sub->id, $errors, $message);
            }
        }
    }

    // phpcs:disable Squiz.Commenting.FunctionCommentThrowTag.Missing
    /**
     * Update account form processing for asynchronous gateways.
     *
     * @return void
     */
    public function process_update_account_form_ajax()
    {
        try {
            $options         = MeprOptions::fetch();
            $subscription_id = (int) $_POST['mepr_subscription_id'] ?? 0;

            if (empty($subscription_id)) {
                throw new MeprGatewayException(__('Bad request', 'memberpress'));
            }

            if (!is_user_logged_in()) {
                throw new MeprGatewayException(__('Sorry, you must be logged in to do this.', 'memberpress'));
            }

            if (!check_ajax_referer('mepr_process_update_account_form', false, false)) {
                throw new MeprGatewayException(__('Security check failed.', 'memberpress'));
            }

            $sub = new MeprSubscription((int) $_POST['mepr_subscription_id'] ?? 0);

            if (!($sub->id > 0)) {
                throw new MeprGatewayException(__('Subscription not found', 'memberpress'));
            }

            $usr = $sub->user();

            if ($usr->ID !== get_current_user_id()) {
                throw new MeprGatewayException(__('This subscription is for another user.', 'memberpress'));
            }

            $pm = $sub->payment_method();

            if (!$pm instanceof MeprBaseRealAjaxGateway) {
                throw new MeprGatewayException(__('Invalid payment gateway', 'memberpress'));
            }

            $pm->process_update_account_form_ajax($sub);

            wp_send_json_success(
                add_query_arg(
                    array_map(
                        'rawurlencode',
                        [
                            'action'  => 'update',
                            'sub'     => $sub->id,
                            'message' => __('Your account information was successfully updated.', 'memberpress'),
                        ]
                    ),
                    $options->account_page_url()
                )
            );
        } catch (Exception $e) {
            wp_send_json_error($e->getMessage());
        }
    }
    // phpcs:enable Squiz.Commenting.FunctionCommentThrowTag.Missing

    /**
     * Upgrades a user's subscription.
     *
     * @return void
     */
    public function upgrade()
    {
        $sub = new MeprSubscription(intval(wp_unslash($_GET['sub'] ?? 0)));
        $prd = $sub->product();
        $grp = $prd->group();

        $mepr_options = MeprOptions::fetch();
        $redirect_url = ($grp && is_object($grp) && method_exists($grp, 'url'))
            ? $grp->url()
            : $mepr_options->account_page_url();

        // TODO: Uyeah, we may want to come up with a more elegant solution here
        // for now we have to do a js redirect because we're in mid-page render.
        ?>
    <script>
      top.window.location = '<?php echo esc_js(esc_url_raw($redirect_url)); ?>';
    </script>
        <?php
    }

    /**
     * Shortcode for displaying the account form.
     *
     * @param array  $atts    Shortcode attributes.
     * @param string $content Content within the shortcode.
     *
     * @return string The account form HTML.
     */
    public function account_form_shortcode($atts, $content = '')
    {
        // No need to validate anything as the below function already
        // does all the validations. This is just a wrapper.
        return $this->display_account_form($content, $atts);
    }

    /**
     * Displays the account form.
     *
     * @param string $content Content within the form.
     * @param array  $atts    Attributes for the form.
     *
     * @return string The account form HTML.
     */
    public function display_account_form($content = '', $atts = [])
    {
        global $post;

        // Static var to prevent duplicate token issues with Stripe.
        static $new_content;
        static $content_length;

        // Init this posts static values.
        if (!isset($new_content) || empty($new_content) || !isset($content_length)) {
            $new_content    = '';
            $content_length = -1;
        }

        if ($new_content && strlen($content) === $content_length) {
            return $new_content;
        }

        $content_length = strlen($content);

        if (MeprUtils::is_user_logged_in()) {
            ob_start();
            MeprAccountCtrl::render($atts);
            $content .= ob_get_clean();
        } else {
            $content = do_shortcode(MeprRulesCtrl::unauthorized_message($post));
        }

        $new_content = $content;

        return $new_content;
    }

    /**
     * Retrieves account links for the user.
     *
     * @return string The account links HTML.
     */
    public function get_account_links()
    {
        $mepr_options = MeprOptions::fetch();
        ob_start();

        if (MeprUtils::is_user_logged_in()) {
            $account_url = $mepr_options->account_page_url();
            $logout_url  = MeprUtils::logout_url();
            if (MeprReadyLaunchCtrl::template_enabled('account')) {
                MeprView::render('/readylaunch/account/logged_in_template', get_defined_vars());
            } else {
                MeprView::render('/account/logged_in_template', get_defined_vars());
            }
        } else {
            $login_url = MeprUtils::login_url();
            if (MeprReadyLaunchCtrl::template_enabled('account')) {
                MeprView::render('/readylaunch/account/logged_out_template', get_defined_vars());
            } else {
                MeprView::render('/account/logged_out_template', get_defined_vars());
            }
        }

        return ob_get_clean();
    }

    /**
     * Outputs account metadata based on attributes.
     *
     * @param array  $atts    Attributes for outputting metadata.
     * @param string $content Content within the metadata.
     *
     * @return string The account metadata.
     */
    public function output_account_meta($atts = [], $content = '')
    {
        global $mepr_options, $user_ID;

        if ((int)$user_ID < 1 || !isset($atts['field'])) {
            return '';
        }

        $ums      = MeprUtils::get_formatted_usermeta($user_ID);
        $usermeta = [];

        if (!empty($ums)) {
            foreach ($ums as $umkey => $umval) {
                $usermeta["{$umkey}"] = $umval;
            }
        }

        // Get some additional params yo.
        $userdata = get_userdata($user_ID);

        foreach ($userdata->data as $key => $value) {
            $usermeta[$key] = $value;
        }

        // We can begin to define more custom return cases in here...
        switch ($atts['field']) {
            case 'full_name':
                return ucfirst($usermeta['first_name']) . ' ' . ucfirst($usermeta['last_name']);
            break;
            case 'full_name_last_first':
                return ucfirst($usermeta['last_name']) . ', ' . ucfirst($usermeta['first_name']);
            break;
            case 'first_name_last_initial':
                return ucfirst($usermeta['first_name']) . ' ' . ucfirst($usermeta['last_name'][0]) . '.';
            break;
            case 'last_name_first_initial':
                return ucfirst($usermeta['last_name']) . ', ' . ucfirst($usermeta['first_name'][0]) . '.';
            break;
            case 'user_registered':
                return MeprAppHelper::format_date($usermeta[$atts['field']]);
            break;
            case 'mepr_user_message':
                return wpautop(stripslashes(do_shortcode($usermeta[$atts['field']])));
            break;
            default:
                // Make sure field exists and is renderable; never return a passed-through object/array (fatals on string coercion).
                if (isset($usermeta[$atts['field']]) && is_scalar($usermeta[$atts['field']]) && !empty($usermeta[$atts['field']])) {
                    return $usermeta[$atts['field']];
                }
                break;
        }
    }

    /**
     * Saves a new password for the user.
     *
     * @param integer $user_id          The user ID.
     * @param string  $new_pass         The new password.
     * @param string  $new_pass_confirm The new password confirmation.
     *
     * @return void
     */
    public function save_new_password($user_id, $new_pass, $new_pass_confirm)
    {
        $mepr_options = MeprOptions::fetch();
        $account_url  = $mepr_options->account_page_url();
        $delim        = MeprAppCtrl::get_param_delimiter_char($account_url);

        $user = MeprUtils::get_currentuserinfo();

        // Check password strength first.
        if ($mepr_options->enforce_strong_password && isset($_POST['mp-pass-strength']) && (int)$_POST['mp-pass-strength'] < MeprZxcvbnCtrl::get_required_int()) {
            MeprUtils::wp_redirect($account_url . $delim . 'action=newpassword&error=weak');
        }

        if ($user_id && $user && ($user->ID === $user_id)) {
            // Check password length.
            if (! MeprUser::validate_password_length($new_pass)) {
                MeprUtils::wp_redirect($account_url . $delim . 'action=newpassword&error=failed');
            }

            if (($new_pass === $new_pass_confirm) && !empty($new_pass)) {
                $user->set_password($new_pass);
                $user->store();
                MeprUtils::wp_redirect($account_url . $delim . 'action=home&message=password_updated');
            }
        }

        MeprUtils::wp_redirect($account_url . $delim . 'action=newpassword&error=failed');
    }

    /**
     * Save account profile fields for ReadyLaunch account template
     *
     * @return void
     */
    public function save_profile_fields()
    {
        // Check for nonce security.
        if (isset($_POST['nonce']) && ! wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'mepr_account_update')) {
            die('Busted!');
        }

        // Since we use user_* for these, we need to artifically set the $_POST keys correctly for this to work.
        if (isset($_POST['user_first_name']) && ( !isset($_POST['first_name']) || empty($_POST['first_name']))) {
            // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
            $_POST['first_name'] = (!empty($_POST['user_first_name'])) ? MeprUtils::sanitize_name_field(wp_unslash($_POST['user_first_name'])) : '';
            unset($_POST['user_first_name']);
        }

        if (isset($_POST['user_last_name']) && (!isset($_POST['last_name']) || empty($_POST['last_name']))) {
            // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
            $_POST['last_name'] = (!empty($_POST['user_last_name'])) ? MeprUtils::sanitize_name_field(wp_unslash($_POST['user_last_name'])) : '';
            unset($_POST['user_last_name']);
        }

        $field_key    = array_map('sanitize_key', array_merge(array_keys($_POST), array_keys($_FILES)));
        $current_user = MeprUtils::get_currentuserinfo();

        $errors = MeprHooks::apply_filters('mepr_validate_account_ajax', [], $current_user, $field_key);

        if (empty($errors)) {
            if (isset($_POST['user_email']) && !empty($_POST['user_email'])) {
                $new_email = sanitize_email(wp_unslash($_POST['user_email']));

                if ($current_user->user_email !== $new_email) {
                    $this->maybe_update_username_to_email($current_user, $new_email);
                    $current_user->user_email = $new_email;
                    $current_user->store();
                    MeprHooks::do_action('mepr_update_new_user_email', $current_user);
                }
            }

            MeprUsersCtrl::save_extra_profile_fields($current_user->ID, true, false, false, $field_key);

            MeprHooks::do_action('mepr_save_account', $current_user);

            wp_send_json_success();
        } else {
            wp_send_json_error($errors);
        }
    }


    /**
     * Generates a shortcode output for offline gateway payment instructions.
     *
     * @param array  $atts    Shortcode attributes, including 'gateway_id'.
     * @param string $content Content within the shortcode.
     *
     * @return string The payment instructions or empty string if conditions aren't met.
     */
    public function offline_gateway_instructions($atts = [], $content = '')
    {
        if (!isset($atts['gateway_id']) || empty($atts['gateway_id'])) {
            return '';
        }

        if (isset($_GET['trans_num']) && !empty($_GET['trans_num'])) {
            $txn = MeprTransaction::get_one_by_trans_num(sanitize_text_field(wp_unslash($_GET['trans_num'])));
        } elseif (isset($_GET['transaction_id']) && !empty($_GET['transaction_id'])) {
            $txn = MeprTransaction::get_one(intval(wp_unslash($_GET['transaction_id'])));
        } else {
            return '';
        }

        if (!isset($txn->gateway) || $txn->gateway !== $atts['gateway_id']) {
            return '';
        }

        return do_shortcode($content);
    }

    /**
     * Modifies the account page title in the loop.
     *
     * @param string $title The current title.
     *
     * @return string The modified title.
     */
    public function account_page_the_title($title)
    {
        if (!in_the_loop() || !MeprHooks::apply_filters('mepr_account_nav_page_titles', false)) {
            return $title;
        }

        return $this->account_page_title($title);
    }
    /**
     * Modifies the account page browser title.
     *
     * @param string $title The current browser title.
     *
     * @return string The modified browser title.
     */
    public function account_page_browser_title($title)
    {
        if (!MeprHooks::apply_filters('mepr_account_nav_broswer_titles', false)) {
            return $title;
        }

        return $this->account_page_title($title);
    }
    /**
     * Modifies the account page title based on the action.
     *
     * @param string $title The current title.
     *
     * @return string The modified title.
     */
    public function account_page_title($title)
    {
        global $post;

        // Only apply the title changes on the account nave pages if it is turned on
        // and buddy press intregration is not installed.
        if (class_exists('MpBuddyPress')) {
            return $title;
        }

        // If we don't have a post, just return the title.
        if (!isset($post) || !isset($post->ID) || $post->ID <= 0) {
            return $title;
        }

        $action      = isset($_REQUEST['action']) ? sanitize_text_field(wp_unslash($_REQUEST['action'])) : '';
        $title_after = '';
        $sep         = ' ' . apply_filters('document_title_separator', '-') . ' '; // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHookname

        if (MeprUser::is_account_page($post)) {
            switch ($action) {
                case 'subscriptions':
                    $title_after = MeprHooks::apply_filters('mepr_account_subscriptions_title', _x('Subscriptions', 'ui', 'memberpress'));
                    break;
                case 'payments':
                    $title_after = MeprHooks::apply_filters('mepr_account_payments_title', _x('Payments', 'ui', 'memberpress'));
                    break;
                case 'courses':
                    $title_after = MeprHooks::apply_filters('mepr_account_courses_title', _x('Courses', 'ui', 'memberpress'));
                    break;
                default:
                    // For custom tabs on account page.
                    $title_after = MeprHooks::apply_filters('mepr_custom_account_nav_title', '', $action);
                    break;
            }
        }

        return !empty($title_after) ? $title . $sep . $title_after : $title;
    }
}
