<?php

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

use MemberPress\GroundLevel\Support\Time;

class MeprGroupsCtrl extends MeprCptCtrl
{
    /**
     * Load hooks for the groups management.
     *
     * @return void
     */
    public function load_hooks()
    {
        add_action('admin_init', 'MeprGroup::cleanup_db');
        add_action('admin_enqueue_scripts', 'MeprGroupsCtrl::enqueue_scripts');
        add_action('manage_pages_custom_column', 'MeprGroupsCtrl::custom_columns', 10, 2);
        add_action('save_post', 'MeprGroupsCtrl::save_postdata');
        add_action('wp_trash_post', 'MeprGroupsCtrl::trash_group_remove_memberships');
        add_action('wp_ajax_mepr_is_product_already_in_group', 'MeprGroupsCtrl::is_product_already_in_group');
        add_action('mepr_group_fallback_membership_deleted', 'MeprGroupsCtrl::remove_fallback_memberships', 10, 2);
        add_action('mepr_group_fallback_membership_changed', 'MeprGroupsCtrl::update_fallback_memberships', 10, 2);
        add_action('mepr_txn_status_complete', [$this, 'expire_fallback']);
        add_action('mepr_txn_status_confirmed', [$this, 'expire_fallback']);
        add_action('mepr_txn_status_refunded', [$this, 'create_fallback']);
        add_action('mepr_transaction_expired', [$this, 'create_fallback'], 10, 2);
        add_action('wp_enqueue_scripts', 'MeprGroupsCtrl::frontend_enqueue_scripts');
        add_filter('the_content', 'MeprGroupsCtrl::render_pricing_boxes', 10);
        add_filter('manage_edit-memberpressgroup_columns', 'MeprGroupsCtrl::columns');
        add_filter('template_include', 'MeprGroupsCtrl::template_include');
        add_action('mepr_txn_status_failed', [$this, 'create_fallback']);

        MeprHooks::add_shortcode('mepr_group_price_boxes', 'MeprGroupsCtrl::shortcode_group_price_boxes');

        // Cleanup list view.
        add_filter('views_edit-' . MeprGroup::$cpt, 'MeprAppCtrl::cleanup_list_view');
    }

    /**
     * Register the custom post type for groups.
     *
     * @return void
     */
    public function register_post_type()
    {
        $mepr_options = MeprOptions::fetch();
        $this->cpt    = (object)[
            'slug'   => MeprGroup::$cpt,
            'config' => [
                'labels'               => [
                    'name'               => __('Groups', 'memberpress'),
                    'singular_name'      => __('Group', 'memberpress'),
                    'add_new'            => __('Add New', 'memberpress'),
                    'add_new_item'       => __('Add New Group', 'memberpress'),
                    'edit_item'          => __('Edit Group', 'memberpress'),
                    'new_item'           => __('New Group', 'memberpress'),
                    'view_item'          => __('View Group', 'memberpress'),
                    'search_items'       => __('Search Group', 'memberpress'),
                    'not_found'          => __('No Group found', 'memberpress'),
                    'not_found_in_trash' => __('No Group found in Trash', 'memberpress'),
                    'parent_item_colon'  => __('Parent Group:', 'memberpress'),
                ],
                'public'               => true,
                'show_ui'              => true,
                'show_in_menu'         => 'memberpress',
                'capability_type'      => 'page',
                'hierarchical'         => true,
                'register_meta_box_cb' => 'MeprGroupsCtrl::add_meta_boxes',
                'rewrite'              => [
                    'slug'       => $mepr_options->group_pages_slug,
                    'with_front' => false,
                ],
                'supports'             => ['title', 'editor', 'page-attributes', 'comments', 'thumbnail'],
            ],
        ];
        register_post_type($this->cpt->slug, $this->cpt->config);
    }

    /**
     * Render pricing boxes for the group.
     *
     * @param  string  $content The existing content.
     * @param  boolean $manual  Whether to render manually.
     * @return string
     */
    public static function render_pricing_boxes($content, $manual = false)
    {
        $current_post = MeprUtils::get_current_post();

        // This isn't a post? Just return the content then.
        if ($current_post === false) {
            return $content;
        }

        // Stop rendering pricing box on Admin side.
        if (is_admin()) {
            return $content;
        }

        // WARNING the_content CAN be run more than once per page load
        // so this static var prevents stuff from happening twice
        // like cancelling a subscr or resuming etc...
        static $already_run    = [];
        static $new_content    = [];
        static $content_length = [];
        // Init this posts static values.
        if (!isset($new_content[$current_post->ID]) || empty($new_content[$current_post->ID])) {
            $already_run[$current_post->ID]    = false;
            $new_content[$current_post->ID]    = '';
            $content_length[$current_post->ID] = -1;
        }

        if ($already_run[$current_post->ID] && strlen($content) === $content_length[$current_post->ID] && !$manual) { // Shortcode may pass.
            return $new_content[$current_post->ID];
        }

        $content_length[$current_post->ID] = strlen($content);
        $already_run[$current_post->ID]    = true;

        if (isset($current_post) && is_a($current_post, 'WP_Post') && $current_post->post_type === MeprGroup::$cpt) {
            $group = new MeprGroup($current_post->ID);

            // Short circuiting for any of the following reasons.
            if (
                $group->ID === null || // Bad group for some reason.
                (!$manual && $group->manual_append_price_boxes()) || // Show manually and the_content filter are enabled.
                ($manual && !$group->manual_append_price_boxes())
            ) { // Show manually and do_shortcode are disabled
                // See notes above.
                $new_content[$current_post->ID] = $content;
                return $new_content[$current_post->ID];
            }

            ob_start();
            self::display_pricing_boxes($group);
            $content .= ob_get_clean();
        }

        // See notes above.
        $new_content[$current_post->ID] = $content;
        return $new_content[$current_post->ID];
    }

    /**
     * Enqueue modern template assets on the frontend.
     *
     * @return void
     */
    public static function frontend_enqueue_scripts()
    {
        global $post;

        if (!isset($post) || !is_a($post, 'WP_Post')) {
            return;
        }

        $group = MeprGroup::is_group_page($post);

        if ($group === false) {
            return;
        }

        self::enqueue_modern_template_assets($group);
    }

    /**
     * Enqueue modern template CSS and JS assets for a group.
     *
     * @param  MeprGroup $group The group object.
     * @return void
     */
    public static function enqueue_modern_template_assets($group)
    {
        if (empty($group->modern_template)) {
            return;
        }

        $valid_templates = array_keys(MeprGroup::modern_templates());
        if (!in_array($group->modern_template, $valid_templates, true)) {
            return;
        }

        // Enqueue modern base + template CSS.
        wp_enqueue_style('mepr-modern-base-css', MEPR_CSS_URL . '/modern_templates/base.css', [], MEPR_VERSION);
        wp_enqueue_style(
            'mepr-modern-template-css',
            MEPR_CSS_URL . '/modern_templates/' . $group->modern_template . '.css',
            ['mepr-modern-base-css'],
            MEPR_VERSION
        );

        // Enqueue Splide if carousel is enabled.
        $carousel_deps = [];
        if ($group->carousel_enabled) {
            wp_enqueue_style('mepr-splide-css', MEPR_CSS_URL . '/vendor/splide-core.min.css', [], MEPR_VERSION);
            wp_enqueue_script('mepr-splide-js', MEPR_JS_URL . '/vendor/splide.min.js', [], MEPR_VERSION, true);
            $carousel_deps[] = 'mepr-splide-js';
        }

        // Always enqueue carousel JS (handles no-carousel class when disabled).
        wp_enqueue_script(
            'mepr-pricing-carousel-js',
            MEPR_JS_URL . '/pricing_carousel.js',
            $carousel_deps,
            MEPR_VERSION,
            true
        );
    }

    /**
     * Display pricing boxes for the group.
     *
     * @param  MeprGroup   $group The group object.
     * @param  string|null $theme The theme to use.
     * @param  array       $args  Additional arguments.
     * @return void
     */
    public static function display_pricing_boxes($group, $theme = null, $args = [])
    {
        if (
            MeprReadyLaunchCtrl::template_enabled('pricing')
            || MeprAppHelper::has_block('memberpress/pro-pricing-table')
            || MeprHooks::apply_filters('mepr_pro_templates_has_pricing_block', false)
        ) {
            MeprView::render('/readylaunch/groups/front_groups_page', get_defined_vars());
        } elseif (!empty($group->modern_template)) {
            // Enqueue here as well for shortcode usage on non-group pages where frontend_enqueue_scripts won't fire.
            self::enqueue_modern_template_assets($group);
            MeprView::render('/groups/front_groups_modern', get_defined_vars());
        } else {
            MeprView::render('/groups/front_groups_page', get_defined_vars());
        }
    }

    /**
     * Define the columns for the group list table.
     *
     * @param  array $columns The existing columns.
     * @return array
     */
    public static function columns($columns)
    {
        $columns = [
            'cb'             => '<input type="checkbox" />',
            'ID'             => __('ID', 'memberpress'),
            'title'          => __('Group Title', 'memberpress'),
            'url'            => __('URL', 'memberpress'),
            'group-products' => __('Memberships in Group', 'memberpress'),
        ];
        return $columns;
    }

    /**
     * Render custom columns in the group list table.
     *
     * @param  string  $column  The name of the column.
     * @param  integer $post_id The ID of the post.
     * @return void
     */
    public static function custom_columns($column, $post_id)
    {
        $group = new MeprGroup($post_id);

        if ($group->ID !== null) {
            if ('ID' === $column) {
                echo esc_html($group->ID);
            } elseif ('group-products' === $column) {
                echo esc_html(implode(', ', $group->products('titles')));
            } elseif ('url' === $column) {
                echo esc_html($group->url());
            }
        }
    }

    // Template selection.
    /**
     * Template selection
     *
     * @param  string $template The current template.
     * @return string
     */
    public static function template_include($template)
    {
        global $post, $wp_query;

        if (!is_singular()) {
            return $template;
        }

        if (isset($post) && is_a($post, 'WP_Post') && $post->post_type === MeprGroup::$cpt) {
            $group = new MeprGroup($post->ID);

            if (!$group->pricing_page_disabled) {
                $new_template = $group->get_page_template();
            } elseif ($group->pricing_page_disabled && !empty($group->alternate_group_url)) {
                MeprUtils::wp_redirect($group->alternate_group_url);
            } else {
                $wp_query->is_404 = true;
                $new_template     = locate_template(['404.php']);
            }
        }

        if (isset($new_template) && !empty($new_template)) {
            return $new_template;
        }

        return $template;
    }

    /**
     * Add meta boxes for the group edit screen.
     *
     * @return void
     */
    public static function add_meta_boxes()
    {
        global $post_id;
        $group = new MeprGroup($post_id);

        add_meta_box(
            'memberpress-group-meta',
            __('Group Options', 'memberpress'),
            'MeprGroupsCtrl::group_meta_box',
            MeprGroup::$cpt,
            'normal',
            'high',
            ['group' => $group]
        );
        add_meta_box(
            'memberpress-custom-template',
            __('Custom Page Template', 'memberpress'),
            'MeprGroupsCtrl::custom_page_template',
            MeprGroup::$cpt,
            'side',
            'default',
            ['group' => $group]
        );
    }

    /**
     * Save the group's metadata when the post is saved.
     *
     * @param  integer $post_id The ID of the post being saved.
     * @return mixed
     */
    public static function save_postdata($post_id)
    {
        $post           = get_post($post_id);
        $fallback_state = 'unchanged';

        $nonce_value = (isset($_POST[MeprGroup::$nonce_str]))
            ? sanitize_text_field(wp_unslash($_POST[MeprGroup::$nonce_str]))
            : '';
        if (!wp_verify_nonce($nonce_value, MeprGroup::$nonce_str . wp_salt())) {
            return $post_id; // Nonce prevents meta data from being wiped on move to trash.
        }

        if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
            return $post_id;
        }

        if (defined('DOING_AJAX')) {
            return;
        }

        if (!empty($post) && $post->post_type === MeprGroup::$cpt) {
            $group                            = new MeprGroup($post_id);
            $group->pricing_page_disabled     = isset($_POST[MeprGroup::$pricing_page_disabled_str]);
            $group->disable_change_plan_popup = isset($_POST[MeprGroup::$disable_change_plan_popup_str]);
            $group->is_upgrade_path           = isset($_POST[MeprGroup::$is_upgrade_path_str]);
            $group->upgrade_path_reset_period = isset($_POST[MeprGroup::$upgrade_path_reset_period_str]);
            // Coupon fields are owned by MeprCouponsCtrl::save_group_postdata
            // (save_post priority 11), so stripped builds cannot wipe them.
            // $group->group_page_style_options = self::get_style_options_array();
            // Handle modern vs legacy template type.
            $template_type = sanitize_text_field(
                wp_unslash($_POST['_mepr_group_template_type'] ?? 'legacy')
            );

            if ($template_type === 'modern') {
                $raw_template           = sanitize_text_field(
                    wp_unslash($_POST[MeprGroup::$modern_template_str] ?? '')
                );
                $valid_templates        = array_keys(MeprGroup::modern_templates());
                $group->modern_template = in_array($raw_template, $valid_templates, true)
                    ? $raw_template : '';

                // Sanitize modern template options.
                $raw_options = isset($_POST[MeprGroup::$modern_template_options_str])
                    ? (array) $_POST[MeprGroup::$modern_template_options_str]
                    : [];

                $group->modern_template_options = [
                    'primary_color'     => sanitize_hex_color($raw_options['primary_color'] ?? '#2563eb') ?: '#2563eb',
                    'accent_color'      => sanitize_hex_color($raw_options['accent_color'] ?? '#16a34a') ?: '#16a34a',
                    'button_color'      => sanitize_hex_color($raw_options['button_color'] ?? '#2563eb') ?: '#2563eb',
                    'btn_text_color'    => sanitize_hex_color($raw_options['btn_text_color'] ?? '#ffffff') ?: '#ffffff',
                    'text_color'        => sanitize_hex_color($raw_options['text_color'] ?? '#1e293b') ?: '#1e293b',
                    'price_color'       => sanitize_hex_color($raw_options['price_color'] ?? '#0f172a') ?: '#0f172a',
                    'hl_text_color'     => sanitize_hex_color($raw_options['hl_text_color'] ?? '#ffffff') ?: '#ffffff',
                    'hl_price_color'    => sanitize_hex_color($raw_options['hl_price_color'] ?? '#ffffff') ?: '#ffffff',
                    'hl_btn_color'      => sanitize_hex_color(
                        $raw_options['hl_btn_color'] ?? '#16a34a'
                    ) ?: '#16a34a',
                    'hl_btn_text_color' => sanitize_hex_color(
                        $raw_options['hl_btn_text_color'] ?? '#ffffff'
                    ) ?: '#ffffff',
                    'border_radius'     => in_array(
                        ($raw_options['border_radius'] ?? ''),
                        ['none', 'subtle', 'rounded'],
                        true
                    ) ? $raw_options['border_radius'] : 'rounded',
                    'shadow'            => in_array(
                        ($raw_options['shadow'] ?? ''),
                        ['none', 'subtle', 'prominent'],
                        true
                    ) ? $raw_options['shadow'] : 'subtle',
                ];

                $group->carousel_enabled        = isset($_POST[MeprGroup::$carousel_enabled_str]);
                $group->carousel_arrows_outside = isset($_POST[MeprGroup::$carousel_arrows_outside_str]);
            } else {
                $group->modern_template         = '';
                $group->carousel_enabled        = false;
                $group->carousel_arrows_outside = false;
            }

            $group->group_theme                   = sanitize_text_field(
                wp_unslash($_POST[MeprGroup::$group_theme_str] ?? '')
            );
            $group->page_button_class             = sanitize_text_field(
                wp_unslash($_POST[MeprGroup::$page_button_class_str] ?? '')
            );
            $group->page_button_highlighted_class = sanitize_text_field(
                wp_unslash($_POST[MeprGroup::$page_button_highlighted_class_str] ?? '')
            );
            $group->page_button_disabled_class    = sanitize_text_field(
                wp_unslash($_POST[MeprGroup::$page_button_disabled_class_str] ?? '')
            );
            $group->alternate_group_url           = sanitize_text_field(
                wp_unslash($_POST[MeprGroup::$alternate_group_url_str] ?? '')
            );
            self::store_chosen_products($group->ID);
            $group->use_custom_template = isset($_POST['_mepr_use_custom_template']);
            $group->custom_template     = isset($_POST['_mepr_custom_template'])
                ? sanitize_text_field(wp_unslash($_POST['_mepr_custom_template']))
                : '';

            $orig_fallback_membership = $group->fallback_membership;
            $fallback_membership      = sanitize_text_field(
                wp_unslash($_POST[MeprGroup::$fallback_membership_str] ?? '')
            );

            if (empty($_POST[MeprGroup::$fallback_membership_str])) {
                if (!empty($orig_fallback_membership)) {
                    // Fallback changed to default or none.
                    $fallback_state = 'deleted';
                }
            } else {
                if (!empty($orig_fallback_membership) && $orig_fallback_membership !== $fallback_membership) {
                    // Fallback changed to a new product/membership.
                    $fallback_state = 'changed';
                }
            }

            $group->fallback_membership = $fallback_membership;
            $group->store_meta();
            // Let's handle the $fallback_state changes through hooks.
            MeprHooks::do_action("mepr_group_fallback_membership_{$fallback_state}", $orig_fallback_membership, $group);

            // Some themes rely on this meta key to be set to use the custom template, and they don't use locate_template.
            if ($group->use_custom_template && !empty($group->custom_template)) {
                update_post_meta($group->ID, '_wp_page_template', $group->custom_template);
            } elseif (apply_filters('mepr_cpt_allow_resetting_to_default_template', true, $group)) {
                update_post_meta($group->ID, '_wp_page_template', '');
            }
        }
    }

    /**
     * Remove memberships when a group is trashed.
     *
     * @param  integer $id The ID of the group being trashed.
     * @return void
     */
    public static function trash_group_remove_memberships($id)
    {
        global $post_type, $wpdb;
        if ($post_type !== MeprGroup::$cpt) {
            return;
        }
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery
        $wpdb->query($wpdb->prepare(
            "DELETE FROM {$wpdb->postmeta} WHERE meta_key = %s AND meta_value = %d",
            MeprProduct::$group_id_str,
            $id
        ));
    }

    // It's not used anywhere
    // public static function get_style_options_array() {
    // $styles = array();
    // $styles['layout']       = $_POST[MeprGroup::$group_page_layout_str];
    // $styles['style']        = $_POST[MeprGroup::$group_page_style_str];
    // $styles['button_size']  = $_POST[MeprGroup::$group_page_button_size_str];
    // $styles['bullet_style'] = $_POST[MeprGroup::$group_page_bullet_style_str];
    // $styles['font_style']   = $_POST[MeprGroup::$group_page_font_style_str];
    // $styles['font_size']    = $_POST[MeprGroup::$group_page_font_size_str];
    // $styles['button_color'] = $_POST[MeprGroup::$group_page_button_color_str];
    // return $styles;
    // }.
    /**
     * Store chosen products for the group.
     *
     * @param  integer $group_id The ID of the group.
     * @return void
     */
    public static function store_chosen_products($group_id)
    {
        if (isset($_POST[MeprGroup::$products_str]['product'])) {
            // Zero out old memberships before assigning the new ones.
            self::zero_out_old_products($group_id);

            for ($index = 0; $index < (count($_POST[MeprGroup::$products_str]['product']) - 1); $index++) {
                $product_id = intval(sanitize_key($_POST[MeprGroup::$products_str]['product'][$index] ?? 0));
                $prd        = new MeprProduct($product_id);

                if ($prd->ID) {
                    $prd->group_id    = $group_id;
                    $prd->group_order = $index;
                    $prd->store_meta();
                }
            }
        }
    }

    // Deletes all memberships from the given group. We purge memberships before saving the new one's.
    /**
     * Zero out old products for the group.
     *
     * @param  integer $group_id The ID of the group.
     * @return void
     */
    public static function zero_out_old_products($group_id)
    {
        $group    = new MeprGroup($group_id);
        $products = $group->products();

        if (!empty($products)) {
            foreach ($products as $product) {
                $product->group_id = 0;
                $product->store_meta();
            }
        }
    }

    /**
     * Display the group meta box.
     *
     * @param  WP_Post $post The current post object.
     * @param  array   $args Additional arguments.
     * @return void
     */
    public static function group_meta_box($post, $args)
    {
        // Don't use $post here, it is null on new group - use args instead.
        $group        = $args['args']['group'];
        $mepr_options = MeprOptions::fetch();

        MeprView::render('/admin/groups/form', get_defined_vars());
    }

    /**
     * Display the custom page template meta box.
     *
     * @param  WP_Post $post The current post object.
     * @param  array   $args Additional arguments.
     * @return void
     */
    public static function custom_page_template($post, $args)
    {
        $group = $args['args']['group'];

        MeprView::render('/admin/groups/custom_page_template_form', get_defined_vars());
    }

    /**
     * Enqueue scripts and styles for the group admin page.
     *
     * @param  string $hook The current admin page hook.
     * @return void
     */
    public static function enqueue_scripts($hook)
    {
        global $current_screen;

        if ($current_screen->post_type === MeprGroup::$cpt) {
            wp_enqueue_style('wp-color-picker');

            /*
             * Coupon search assets (Tom Select + admin_coupon_search.js) are
             * owned by MeprCouponsCtrl::admin_enqueue_scripts, which also
             * covers this screen. They must not be dependencies here or a
             * stripped build would silently drop all group admin assets.
             */
            wp_enqueue_style(
                'mepr-groups-css',
                MEPR_CSS_URL . '/admin-groups.css',
                ['mepr-settings-table-css'],
                MEPR_VERSION
            );

            // Enqueue modern template CSS for live preview on group edit screen.
            wp_enqueue_style('mepr-modern-base-css', MEPR_CSS_URL . '/modern_templates/base.css', [], MEPR_VERSION);
            foreach (array_keys(MeprGroup::modern_templates()) as $tpl_slug) {
                wp_enqueue_style(
                    'mepr-modern-tpl-' . $tpl_slug,
                    MEPR_CSS_URL . '/modern_templates/' . $tpl_slug . '.css',
                    ['mepr-modern-base-css'],
                    MEPR_VERSION
                );
            }

            wp_dequeue_script('autosave'); // Disable auto-saving.

            wp_enqueue_script(
                'mepr-groups-js',
                MEPR_JS_URL . '/admin_groups.js',
                ['jquery', 'jquery-ui-sortable', 'mepr-settings-table-js'],
                MEPR_VERSION,
                true
            );

            wp_localize_script(
                'mepr-groups-js',
                'MeprAdminGroups',
                ['readylaunch_enabled' => MeprReadyLaunchCtrl::template_enabled('pricing')]
            );
        }
    }

    /**
     * Check if a product is already in a group via AJAX.
     *
     * @return void
     */
    public static function is_product_already_in_group()
    {
        if (!isset($_POST['product_id'])) {
            esc_html_e('Unknown error has occured.', 'memberpress');
            die();
        }

        $groups = MeprCptModel::all('MeprGroup');

        if (empty($groups)) {
            die();
        }

        foreach ($groups as $g) {
            $group    = new MeprGroup($g->ID);
            $products = $group->products();

            if (empty($products)) {
                continue;
            }

            foreach ($products as $p) {
                if ((int) $p->ID === (int) $_POST['product_id']) {
                    // phpcs:ignore Generic.Files.LineLength.TooLong
                    esc_html_e('This membership already belongs to another group. If you assign it to this group, it will be removed from the other.', 'memberpress');
                    die();
                }
            }
        }

        die(); // No matches so let's kill this thing.
    }

    /**
     * Removes all fallback transactions matching the product and gateway
     * Hook: mepr-group-fallback-membership-deleted
     *
     * @param  integer   $fallback_membership The ID of the fallback membership.
     * @param  MeprGroup $group               The group object.
     * @return void
     */
    public static function remove_fallback_memberships($fallback_membership, $group)
    {
        global $wpdb;
        $mepr_db = MeprDb::fetch();

        MeprUtils::debug_log("Removing fallback memberships for {$fallback_membership}");
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery
        $wpdb->delete(
            $mepr_db->transactions,
            [
                'product_id' => (int) $fallback_membership,
                'gateway'    => MeprTransaction::$fallback_gateway_str,
            ],
            ['%d', '%s']
        );
    }

    /**
     * Updates all fallback transactions matching the product and gateway to the new product/membership
     * Hook: mepr-group-fallback-membership-changed
     *
     * @param  integer   $fallback_membership The ID of the fallback membership.
     * @param  MeprGroup $group               The group object.
     * @return void
     */
    public static function update_fallback_memberships($fallback_membership, $group)
    {
        global $wpdb;
        $mepr_db = MeprDb::fetch();

        MeprUtils::debug_log(
            "Updating fallback memberships for {$fallback_membership} to {$group->fallback_membership}"
        );
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery
        $wpdb->update(
            $mepr_db->transactions,
            ['product_id' => (int) $group->fallback_membership],
            [
                'product_id' => (int) $fallback_membership,
                'gateway'    => MeprTransaction::$fallback_gateway_str,
            ],
            ['%d'],
            ['%d', '%s']
        );
    }

    /**
     * Used to expire the active fallback transaction
     * Hook: mepr-txn-status-complete, mepr-txn-status-confirmed
     *
     * @param  MeprTransaction $txn The transaction object.
     * @return void
     */
    public static function expire_fallback($txn)
    {
        $product = $txn->product();
        $user    = $txn->user();
        $group   = $product->group();

        // Return if product doesn't belong to a group.
        if ($group === false) {
            return;
        }

        $fallback_membership = $group->fallback_membership();

        // No fallback for product or the transaction product is the fallback.
        if ($fallback_membership !== false && $product->ID !== $fallback_membership->ID) {
            // No active subscription found for the group.
            if (
                ($user->subscription_in_group($group))
                || ($user->lifetime_subscription_in_group($group, [MeprTransaction::$fallback_str]))
            ) {
                $fallback_txn = $user->fallback_txn($fallback_membership->ID);
                // No fallback transaction exists.
                if ($fallback_txn !== false) {
                    $fallback_txn->expire();
                }
            }
        }
    }

    /**
     * Used to create a fallback transaction
     * Hooks: mepr-transaction-expired, mepr-txn-status-refunded, mepr-txn-status-failed
     *
     * @param  MeprTransaction $txn        The transaction object.
     * @param  boolean         $sub_status The subscription status.
     * @return void
     */
    public static function create_fallback($txn, $sub_status = false)
    {
        $user    = $txn->user();
        $product = $txn->product();
        $group   = $product->group();

        // Return if product doesn't belong to a group.
        if ($group === false) {
            return;
        }

        $fallback_membership = $group->fallback_membership();

        // No fallback for product or the transaction product is the fallback.
        if ($fallback_membership !== false && $product->ID !== $fallback_membership->ID) {
            // User still has an active subscription in the group.
            if (
                (!$user->is_already_subscribed_to($fallback_membership->ID))
                && (!$user->subscription_in_group($group))
                && (!$user->lifetime_subscription_in_group($group, [MeprTransaction::$fallback_str]))
            ) {
                $fallback_txn_id = $txn->create_fallback_transaction();
                MeprEvent::record('subscription-changed', $txn, $fallback_txn_id);
            }
        }
    }

    /**
     * Shortcode to display group price boxes.
     *
     * @param  array  $atts    The shortcode attributes.
     * @param  string $content The content inside the shortcode.
     * @return string
     */
    public static function shortcode_group_price_boxes($atts, $content = '')
    {
        if (isset($atts['group_id'])) {
            $group = new MeprGroup($atts['group_id']);
            $theme = (isset($atts['theme']) ? $atts['theme'] : null);

            ob_start();

            self::display_pricing_boxes($group, $theme, $atts);

            return ob_get_clean();
        }

        // No validation needed here as the below function does it all
        // This is just a wrapper.
        return self::render_pricing_boxes('', true);
    }
}
