<?php

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

class MeprProduct extends MeprCptModel implements MeprProductInterface
{
    /**
     * Meta key for product price
     *
     * @var string
     */
    public static $price_str = '_mepr_product_price';

    /**
     * Meta key for product period
     *
     * @var string
     */
    public static $period_str = '_mepr_product_period';

    /**
     * Meta key for product period type
     *
     * @var string
     */
    public static $period_type_str = '_mepr_product_period_type';

    /**
     * Meta key for signup button text
     *
     * @var string
     */
    public static $signup_button_text_str = '_mepr_product_signup_button_text';

    /**
     * Meta key for limit cycles
     *
     * @var string
     */
    public static $limit_cycles_str = '_mepr_product_limit_cycles';

    /**
     * Meta key for limit cycles number
     *
     * @var string
     */
    public static $limit_cycles_num_str = '_mepr_product_limit_cycles_num';

    /**
     * Meta key for limit cycles action
     *
     * @var string
     */
    public static $limit_cycles_action_str = '_mepr_product_limit_cycles_action';

    /**
     * Meta key for limit cycles expires after
     *
     * @var string
     */
    public static $limit_cycles_expires_after_str = '_mepr_product_limit_cycles_expires_after';

    /**
     * Meta key for limit cycles expires type
     *
     * @var string
     */
    public static $limit_cycles_expires_type_str = '_mepr_product_limit_cycles_expires_type';

    /**
     * Meta key for trial
     *
     * @var string
     */
    public static $trial_str = '_mepr_product_trial';

    /**
     * Meta key for trial days
     *
     * @var string
     */
    public static $trial_days_str = '_mepr_product_trial_days';

    /**
     * Meta key for trial amount
     *
     * @var string
     */
    public static $trial_amount_str = '_mepr_product_trial_amount';

    /**
     * Meta key for trial once
     *
     * @var string
     */
    public static $trial_once_str = '_mepr_product_trial_once';

    /**
     * Meta key for group ID
     *
     * @var string
     */
    public static $group_id_str = '_mepr_group_id'; // Only one group at a time dude.

    /**
     * Meta key for group order
     *
     * @var string
     */
    public static $group_order_str = '_mepr_group_order'; // Position in group.

    /**
     * Meta key for is highlighted
     *
     * @var string
     */
    public static $is_highlighted_str = '_mepr_product_is_highlighted';

    /**
     * Meta key for who can purchase
     *
     * @var string
     */
    public static $who_can_purchase_str = '_mepr_product_who_can_purchase';

    /**
     * Meta key for have or had
     *
     * @var string
     */
    public static $have_or_had_str = '_mepr_product_purchase';

    /**
     * Meta key for pricing title
     *
     * @var string
     */
    public static $pricing_title_str = '_mepr_product_pricing_title';

    /**
     * Meta key for pricing display
     *
     * @var string
     */
    public static $pricing_display_str = '_mepr_product_pricing_display';

    /**
     * Meta key for pricing show price
     *
     * @var string
     */
    public static $pricing_show_price_str = '_mepr_product_pricing_show_price';

    /**
     * Meta key for custom price
     *
     * @var string
     */
    public static $custom_price_str = '_mepr_product_custom_price';

    /**
     * Meta key for custom price term.
     *
     * @var string
     */
    public static $custom_price_term_str = '_mepr_product_custom_price_term';

    /**
     * Meta key for pricing heading text
     *
     * @var string
     */
    public static $pricing_heading_txt_str = '_mepr_product_pricing_heading_text';

    /**
     * Meta key for pricing footer text
     *
     * @var string
     */
    public static $pricing_footer_txt_str = '_mepr_product_pricing_footer_text';

    /**
     * Meta key for pricing button text
     *
     * @var string
     */
    public static $pricing_button_txt_str = '_mepr_product_pricing_button_text';

    /**
     * Meta key for pricing badge text.
     *
     * @var string
     */
    public static $pricing_badge_txt_str = '_mepr_product_pricing_badge_text';

    /**
     * Meta key for pricing button position
     *
     * @var string
     */
    public static $pricing_button_position_str = '_mepr_product_pricing_button_position';

    /**
     * Meta key for pricing benefits
     *
     * @var string
     */
    public static $pricing_benefits_str = '_mepr_product_pricing_benefits';

    /**
     * Meta key for register price action
     *
     * @var string
     */
    public static $register_price_action_str = '_mepr_register_price_action';

    /**
     * Meta key for register price
     *
     * @var string
     */
    public static $register_price_str = '_mepr_register_price';

    /**
     * Meta key for thank you page enabled
     *
     * @var string
     */
    public static $thank_you_page_enabled_str = '_mepr_thank_you_page_enabled';

    /**
     * Meta key for thank you page type
     *
     * @var string
     */
    public static $thank_you_page_type_str = '_mepr_thank_you_page_type';

    /**
     * Meta key for thank you message
     *
     * @var string
     */
    public static $thank_you_message_str = '_mepr_product_thank_you_message';

    /**
     * Meta key for thank you page ID
     *
     * @var string
     */
    public static $thank_you_page_id_str = '_mepr_product_thank_you_page_id';

    /**
     * Meta key for simultaneous subscriptions
     *
     * @var string
     */
    public static $simultaneous_subscriptions_str = '_mepr_allow_simultaneous_subscriptions';

    /**
     * Meta key for use custom template
     *
     * @var string
     */
    public static $use_custom_template_str = '_mepr_use_custom_template';

    /**
     * Meta key for custom template
     *
     * @var string
     */
    public static $custom_template_str = '_mepr_custom_template';

    /**
     * Meta key for customize payment methods
     *
     * @var string
     */
    public static $customize_payment_methods_str = '_mepr_customize_payment_methods';

    /**
     * Meta key for custom payment methods
     *
     * @var string
     */
    public static $custom_payment_methods_str = '_mepr_custom_payment_methods';

    /**
     * Meta key for customize profile fields
     *
     * @var string
     */
    public static $customize_profile_fields_str = '_mepr_customize_profile_fields';

    /**
     * Meta key for custom profile fields
     *
     * @var string
     */
    public static $custom_profile_fields_str = '_mepr_custom_profile_fields';

    /**
     * Meta key for custom login urls enabled
     *
     * @var string
     */
    public static $custom_login_urls_enabled_str = '_mepr_custom_login_urls_enabled';

    /**
     * Meta key for custom login urls default
     *
     * @var string
     */
    public static $custom_login_urls_default_str = '_mepr_custom_login_urls_default';

    /**
     * Meta key for custom login urls
     *
     * @var string
     */
    public static $custom_login_urls_str = '_mepr_custom_login_urls';

    /**
     * Meta key for expire type
     *
     * @var string
     */
    public static $expire_type_str = '_mepr_expire_type';

    /**
     * Meta key for expire after
     *
     * @var string
     */
    public static $expire_after_str = '_mepr_expire_after';

    /**
     * Meta key for expire unit
     *
     * @var string
     */
    public static $expire_unit_str = '_mepr_expire_unit';

    /**
     * Meta key for expire fixed
     *
     * @var string
     */
    public static $expire_fixed_str = '_mepr_expire_fixed';

    /**
     * Meta key for tax exempt
     *
     * @var string
     */
    public static $tax_exempt_str = '_mepr_tax_exempt';

    /**
     * Meta key for tax class
     *
     * @var string
     */
    public static $tax_class_str = '_mepr_tax_class';

    /**
     * Meta key for allow renewal
     *
     * @var string
     */
    public static $allow_renewal_str = '_mepr_allow_renewal';

    /**
     * Meta key for access URL
     *
     * @var string
     */
    public static $access_url_str = '_mepr_access_url';

    /**
     * Meta key for emails
     *
     * @var string
     */
    public static $emails_str = '_mepr_emails';

    /**
     * Meta key for disable address fields
     *
     * @var string
     */
    public static $disable_address_fields_str = '_mepr_disable_address_fields'; // For free products mostly.

    /**
     * Meta key for cannot purchase message
     *
     * @var string
     */
    public static $cannot_purchase_message_str = '_mepr_cannot_purchase_message';

    /**
     * Meta key for plan code
     *
     * @var string
     */
    public static $plan_code_str = '_mepr_plan_code';

    /**
     * Meta key for nonce
     *
     * @var string
     */
    public static $nonce_str = 'mepr_products_nonce';

    /**
     * Meta key for DB cleanup last run
     *
     * @var string
     */
    public static $last_run_str = 'mepr_products_db_cleanup_last_run';

    /**
     * Custom post type name
     *
     * @var string
     */
    public static $cpt = 'memberpressproduct';

    /**
     * Taxonomy name for product category
     *
     * @var string
     */
    public static $taxonomy_product_category = 'mepr-product-category';

    /**
     * The period types.
     *
     * @var array
     */
    public $period_types;

    /**
     * The limit cycles actions.
     *
     * @var array
     */
    public $limit_cycles_actions;

    /**
     * The expire units.
     *
     * @var array
     */
    public $expire_units;

    /**
     * The register price actions.
     *
     * @var array
     */
    public $register_price_actions;

    /**
     * The pricing displays.
     *
     * @var array
     */
    public $pricing_displays;

    /**
     * The expire types.
     *
     * @var array
     */
    public $expire_types;

    /**
     * Cached applicable coupon codes for this product instance, keyed by MD5 hash of method parameters.
     *
     * @var array<string, string>
     */
    private $applicable_coupon_code_cache = [];

    /**
     * Constructor for the MeprProduct class.
     *
     * @param mixed $obj The object to load.
     */
    public function __construct($obj = null)
    {
        $this->load_cpt(
            $obj,
            self::$cpt,
            [
                'price'                      => 0.00,
                'period'                     => 1,
                'period_type'                => 'lifetime', // Default to lifetime to simplify new membership form.
                'signup_button_text'         => __('Sign Up', 'memberpress'),
                'limit_cycles'               => false,
                'limit_cycles_num'           => 2,
                'limit_cycles_action'        => 'expire',
                'limit_cycles_expires_after' => 1,
                'limit_cycles_expires_type'  => 'days',
                'trial'                      => false,
                'trial_days'                 => 1,
                'trial_amount'               => 0.00,
                'trial_once'                 => true,
                'group_id'                   => 0,
                'group_order'                => 0,
                'is_highlighted'             => false,
                'plan_code'                  => '',
                // The who_can_purchase should be an array of OBJECTS.
                'who_can_purchase'           => [],
                'pricing_title'              => '',
                'pricing_show_price'         => true,
                'pricing_display'            => '',
                'custom_price'               => '',
                'custom_price_term'          => '',
                'pricing_heading_txt'        => '',
                'pricing_footer_txt'         => '',
                'pricing_button_txt'         => '',
                'pricing_badge_txt'          => '',
                'pricing_button_position'    => 'footer',
                // Pricing benefits should be an array of strings.
                'pricing_benefits'           => [],
                'register_price_action'      => 'default',
                'register_price'             => '',
                'thank_you_page_enabled'     => false,
                'thank_you_page_type'        => '',
                'thank_you_message'          => '',
                'thank_you_page_id'          => 0,
                'custom_login_urls_enabled'  => false,
                'custom_login_urls_default'  => '',
                // An array of objects ->url and ->count.
                'custom_login_urls'          => [],
                'expire_type'                => 'none',
                'expire_after'               => 1,
                'expire_unit'                => 'days',
                'expire_fixed'               => '',
                'tax_exempt'                 => false,
                'tax_class'                  => 'standard',
                'allow_renewal'              => false,
                'access_url'                 => '',
                'emails'                     => [],
                'disable_address_fields'     => false, // Mostly for free products.
                'simultaneous_subscriptions' => false,
                'use_custom_template'        => false,
                'custom_template'            => '',
                'customize_payment_methods'  => false,
                'custom_payment_methods'     => [],
                'customize_profile_fields'   => false,
                'custom_profile_fields'      => [],
                'cannot_purchase_message'    => _x('You don\'t have access to purchase this item.', 'ui', 'memberpress'),
            ]
        );

        $this->period_types           = ['weeks','months','years','lifetime'];
        $this->limit_cycles_actions   = ['expire','lifetime', 'expires_after'];
        $this->register_price_actions = ['default', 'hidden', 'custom'];
        $this->expire_types           = ['none','delay','fixed'];
        $this->expire_units           = ['days','weeks','months','years'];
        $this->pricing_displays       = ['auto','custom','none'];

        if (empty($this->pricing_display) && isset($this->pricing_show_price)) {
            $this->pricing_display = $this->pricing_show_price ? 'auto' : 'none';
        }
    }

    /**
     * Validate the product's properties.
     *
     * @return void
     */
    public function validate()
    {
        $this->validate_not_empty($this->post_title, 'title');
        $this->validate_is_currency($this->price, 0.00, null, 'price');
        $this->validate_is_numeric($this->period, 1, 12, 'period');
        $this->validate_is_in_array($this->period_type, $this->period_types, 'period_type');
        $this->validate_not_empty($this->signup_button_text, 'signup_button_text');

        $this->validate_is_bool($this->limit_cycles, 'limit_cycles');
        if ($this->limit_cycles) {
            $this->validate_is_numeric($this->limit_cycles_num, 1, null, 'limit_cycles_num');
            $this->validate_is_in_array($this->limit_cycles_action, $this->limit_cycles_actions, 'limit_cycles_action');
        }

        $this->validate_is_bool($this->trial, 'trial');
        if ($this->trial) {
            $this->validate_is_numeric($this->trial_days, 0.00, null, 'trial_days');
            $this->validate_is_currency($this->trial_amount, 0.00, null, 'trial_amount');
            $this->validate_is_bool($this->trial_once, 'trial_once');
        }

        $this->validate_is_numeric($this->group_id, 0, null, 'group_id');
        $this->validate_is_numeric($this->group_order, 0, null, 'group_order');

        $this->validate_is_bool($this->is_highlighted, 'is_highlighted');
        $this->validate_is_array($this->who_can_purchase, 'who_can_purchase');
        // $this->validate_is_bool($this->pricing_show_price, 'pricing_show_price');
        $this->validate_is_in_array($this->pricing_display, $this->pricing_displays, 'pricing_display');

        // No need to validate these at this time
        // 'pricing_title' => '',
        // 'pricing_heading_txt' => '',
        // 'pricing_footer_txt' => '',
        // 'pricing_button_txt' => '',
        // $this->validate_is_array($this->pricing_benefits, 'pricing_benefits');.
        $this->validate_is_in_array(
            $this->register_price_action,
            $this->register_price_actions,
            'register_price_action'
        );
        $this->validate_is_bool($this->thank_you_page_enabled, 'thank_you_page_enabled');

        // No need to validate
        // 'thank_you_message' => ''.
        $this->validate_is_bool($this->custom_login_urls_enabled, 'custom_login_urls_enabled');

        // No need to validate
        // 'custom_login_urls_default' => ''.
        if ($this->custom_login_urls_enabled) {
            $this->validate_is_array($this->custom_login_urls, 'custom_login_urls');
        }

        $this->validate_is_in_array($this->expire_type, $this->expire_types, 'expire_type');
        if ($this->expire_type === 'delay') {
            $this->validate_is_numeric($this->expire_after, 1, null, 'expire_after');
            $this->validate_is_in_array($this->expire_unit, $this->expire_units, 'expire_unit');
        } elseif ($this->expire_type === 'fixed') {
            $this->validate_is_date($this->expire_fixed, 'expire_fixed');
        }

        $this->validate_is_bool($this->tax_exempt, 'tax_exempt');
        $this->validate_is_bool($this->allow_renewal, 'allow_renewal');

        if (!empty($this->access_url)) {
            $this->validate_is_url($this->access_url, 'access_url');
        }

        $this->validate_is_array($this->emails, 'emails');

        $this->validate_is_bool($this->simultaneous_subscriptions, 'simultaneous_subscriptions');

        $this->validate_is_bool($this->use_custom_template, 'use_custom_template');
        if ($this->use_custom_template) {
            $this->validate_not_empty($this->custom_template, 'custom_template');
        }

        $this->validate_is_bool($this->customize_payment_methods, 'customize_payment_methods');
        if ($this->customize_payment_methods) {
            $this->validate_is_array($this->custom_payment_methods, 'custom_payment_methods');
        }

        $this->validate_is_bool($this->customize_profile_fields, 'customize_profile_fields');
        if ($this->customize_profile_fields) {
            $this->validate_is_array($this->custom_profile_fields, 'custom_profile_fields');
        }
    }

    /**
     * Store the product's metadata.
     *
     * @return void
     */
    public function store_meta()
    {
        $id = $this->ID;

        update_post_meta($id, self::$price_str, MeprUtils::format_float($this->price));
        update_post_meta($id, self::$period_str, $this->period);
        update_post_meta($id, self::$period_type_str, $this->period_type);
        update_post_meta($id, self::$signup_button_text_str, $this->signup_button_text);
        update_post_meta($id, self::$limit_cycles_str, $this->limit_cycles);
        update_post_meta($id, self::$limit_cycles_num_str, $this->limit_cycles_num);
        update_post_meta($id, self::$limit_cycles_action_str, $this->limit_cycles_action);
        update_post_meta($id, self::$limit_cycles_expires_after_str, $this->limit_cycles_expires_after);
        update_post_meta($id, self::$limit_cycles_expires_type_str, $this->limit_cycles_expires_type);
        update_post_meta($id, self::$trial_str, $this->trial);
        update_post_meta($id, self::$trial_days_str, $this->trial_days);
        update_post_meta($id, self::$trial_amount_str, $this->trial_amount);
        update_post_meta($id, self::$trial_once_str, $this->trial_once);
        update_post_meta($id, self::$group_id_str, $this->group_id);
        update_post_meta($id, self::$group_order_str, $this->group_order);
        update_post_meta($id, self::$who_can_purchase_str, $this->who_can_purchase);
        update_post_meta($id, self::$is_highlighted_str, $this->is_highlighted);
        update_post_meta($id, self::$pricing_title_str, $this->pricing_title);
        update_post_meta($id, self::$pricing_display_str, $this->pricing_display);
        update_post_meta($id, self::$custom_price_str, $this->custom_price);
        update_post_meta($id, self::$custom_price_term_str, $this->custom_price_term);
        update_post_meta($id, self::$pricing_heading_txt_str, $this->pricing_heading_txt);
        update_post_meta($id, self::$pricing_footer_txt_str, $this->pricing_footer_txt);
        update_post_meta($id, self::$pricing_button_txt_str, $this->pricing_button_txt);
        update_post_meta($id, self::$pricing_badge_txt_str, $this->pricing_badge_txt);
        update_post_meta($id, self::$pricing_button_position_str, $this->pricing_button_position);
        update_post_meta($id, self::$pricing_benefits_str, $this->pricing_benefits);
        update_post_meta($id, self::$register_price_action_str, $this->register_price_action);
        update_post_meta($id, self::$register_price_str, $this->register_price);
        update_post_meta($id, self::$thank_you_page_enabled_str, $this->thank_you_page_enabled);
        update_post_meta($id, self::$thank_you_page_type_str, $this->thank_you_page_type);
        update_post_meta($id, self::$thank_you_message_str, $this->thank_you_message);
        update_post_meta($id, self::$thank_you_page_id_str, $this->thank_you_page_id);
        update_post_meta($id, self::$custom_login_urls_enabled_str, $this->custom_login_urls_enabled);
        update_post_meta($id, self::$custom_login_urls_default_str, $this->custom_login_urls_default);
        update_post_meta($id, self::$custom_login_urls_str, $this->custom_login_urls);
        update_post_meta($id, self::$expire_type_str, $this->expire_type);
        update_post_meta($id, self::$expire_after_str, $this->expire_after);
        update_post_meta($id, self::$expire_unit_str, $this->expire_unit);
        update_post_meta($id, self::$expire_fixed_str, $this->expire_fixed);
        update_post_meta($id, self::$tax_exempt_str, $this->tax_exempt);
        update_post_meta($id, self::$tax_class_str, $this->tax_class);
        update_post_meta($id, self::$allow_renewal_str, $this->allow_renewal);
        update_post_meta($id, self::$access_url_str, $this->access_url);
        update_post_meta($id, self::$emails_str, $this->emails);
        update_post_meta($id, self::$disable_address_fields_str, $this->disable_address_fields); // Mostly for free products.
        update_post_meta($id, self::$simultaneous_subscriptions_str, $this->simultaneous_subscriptions);
        update_post_meta($id, self::$use_custom_template_str, $this->use_custom_template);
        update_post_meta($id, self::$custom_template_str, $this->custom_template);
        update_post_meta($id, self::$customize_payment_methods_str, $this->customize_payment_methods);
        update_post_meta($id, self::$customize_profile_fields_str, $this->customize_profile_fields);
        update_post_meta($id, self::$cannot_purchase_message_str, $this->cannot_purchase_message);
        update_post_meta($id, self::$plan_code_str, $this->plan_code);

        if ($this->customize_payment_methods) {
            update_post_meta($id, self::$custom_payment_methods_str, $this->custom_payment_methods);
        } else {
            delete_post_meta($id, self::$custom_payment_methods_str);
        }

        if ($this->customize_profile_fields) {
            update_post_meta($id, self::$custom_profile_fields_str, $this->custom_profile_fields);
        } else {
            delete_post_meta($id, self::$custom_profile_fields_str);
        }
    }

    /**
     * Check if the product is prorated.
     *
     * @return boolean
     */
    public function is_prorated()
    {
        $mepr_options = MeprOptions::fetch();
        return($mepr_options->pro_rated_upgrades and $this->is_upgrade_or_downgrade());
    }

    /**
     * Whether this product is tax exempt
     *
     * @return boolean
     */
    public function is_tax_exempt()
    {
        return (bool) $this->tax_exempt;
    }

    /**
     * Get a single product by ID.
     *
     * @param  integer $id The product ID.
     * @return MeprProduct|false
     */
    public static function get_one($id)
    {
        $post = get_post($id);

        if (is_null($post)) {
            return false;
        } else {
            return new MeprProduct($post->ID);
        }
    }

    /**
     * Get all products.
     *
     * @return MeprProduct[]
     */
    public static function get_all()
    {
        $ids = get_posts([
            'post_type'   => self::$cpt,
            'post_status' => 'publish',
            'numberposts' => -1,
            'fields'      => 'ids',
        ]);

        $memberships = [];
        foreach ($ids as $id) {
            $memberships[] = new MeprProduct($id);
        }

        return $memberships;
    }

    /**
     * Get the total number of published products
     *
     * @return integer
     */
    public static function count()
    {
        $counts = wp_count_posts(self::$cpt);
        return (int) ($counts->publish ?? 0);
    }

    /**
     * This presents the price as a float, based on the information contained in
     * $this, the user_id and $coupon_code passed to it.
     *
     * If a user_id and a coupon code is present just adjust the price based on
     * the user first (if any) and then apply the coupon to the remaining price.
     *
     * Coupon code needs to be validated using MeprCoupon::is_valid_coupon_code()
     * before passing a code to this method
     *
     * @param  string|null $coupon_code The coupon code.
     * @param  boolean     $prorate     Whether to prorate the price.
     * @return float
     */
    public function adjusted_price($coupon_code = null, $prorate = true)
    {
        $current_user  = MeprUtils::get_currentuserinfo();
        $product_price = $this->price;
        $mepr_options  = MeprOptions::fetch();

        // Note to future self, we do not want to validate the coupon
        // here as it causes major issues if the coupon has expired
        // or has reached its usage count max. See notes above this method.
        if (!empty($coupon_code)) {
            $coupon = MeprCoupon::get_one_from_code($coupon_code);

            if ($coupon !== false) {
                $product_price = $coupon->apply_discount($product_price, $this->is_one_time_payment(), $this);
            }
        }

        if (!empty($_REQUEST['ca']) || !empty($_REQUEST['mpca_corporate_account_id'])) {
            // Signup is a Corporate Account validated in validate_ca_signup and associate_sub_account.
            $product_price = 0.00;
        } elseif ($prorate && $this->is_one_time_payment() && $this->is_prorated()) {
            $grp = $this->group();

            // Calculate days in this new period.
            $days_in_new_period = $this->days_in_my_period();

            $old_sub = $current_user->subscription_in_group($grp->ID);
            if ($old_sub) {
                $lt = $old_sub->latest_txn();

                if ($lt instanceof MeprTransaction && $lt->id > 0) {
                    $r = MeprUtils::calculate_proration(
                        $lt->amount,
                        $product_price,
                        $old_sub->days_in_this_period(),
                        $days_in_new_period,
                        $old_sub->days_till_expiration(),
                        $grp->upgrade_path_reset_period,
                        $old_sub
                    );

                      // Don't update this below if the latest payment was for 0.00.
                    if (MeprUtils::format_float($lt->amount) > 0.00) {
                        $product_price = $r->proration;
                    }
                }
            } else {
                $txn = $current_user->lifetime_subscription_in_group($grp->ID);
                if ($txn && MeprUtils::format_float($txn->amount) > 0.00) {
                    // Don't update this below if the latest payment was for 0.00.
                    $r = MeprUtils::calculate_proration(
                        $txn->amount,
                        $product_price,
                        $txn->days_in_this_period(),
                        $days_in_new_period,
                        $txn->days_till_expiration(),
                        $grp->upgrade_path_reset_period
                    );

                    $product_price = $r->proration;
                }
            }
        }

        $product_price = MeprHooks::apply_filters('mepr_adjusted_price', $product_price, $coupon_code, $this);

        return MeprUtils::format_float($product_price);
    }

    /**
     * Check if payment is required for the product.
     *
     * @param  string|null $coupon_code The coupon code.
     * @return boolean
     */
    public function is_payment_required($coupon_code = null)
    {
        if ($coupon_code && !MeprCoupon::is_valid_coupon_code($coupon_code, $this->ID)) {
            $coupon_code = null;
        }

        return $this->adjusted_price($coupon_code) > 0.00;
    }

    /**
     * Get auto-apply coupons for this product.
     *
     * @param  boolean $only_applicable Whether to filter only coupons applicable at current time (default true).
     * @param  boolean $use_cache       Whether to use cache for retrieval (cache is always updated).
     * @return array<array<string, mixed>> Array of coupon data arrays (same coupon can appear multiple times with different dates).
     */
    private function get_auto_apply_coupons(bool $only_applicable = true, bool $use_cache = true): array
    {
        $coupons_by_product = MeprProductsHelper::get_auto_apply_coupons_for_products(
            [$this->ID],
            $only_applicable,
            $use_cache
        );

        return $coupons_by_product[$this->ID] ?? [];
    }

    /**
     * Get the currently applicable coupon code for this product.
     *
     * First checks for a coupon code in request parameter (if enabled), then checks auto-apply coupons
     * and returns the first valid coupon code found. Results are cached per instance.
     *
     * @param  boolean $use_coupon_param    Whether to check request parameter for coupon.
     * @param  string  $coupon_param_source Source for coupon parameter: 'GET', 'POST', or 'REQUEST' (default 'GET').
     * @param  integer $user_id             User ID to validate coupon against (default 0, uses current user if 0 or negative).
     * @param  boolean $use_cache           Whether to use cache for retrieval (cache is always updated).
     * @return string The applicable coupon code, or empty string if none found.
     */
    public function get_applicable_coupon_code(
        bool $use_coupon_param = true,
        string $coupon_param_source = 'GET',
        int $user_id = 0,
        bool $use_cache = true
    ): string {
        $user_id = 0 >= (int) $user_id ? get_current_user_id() : $user_id;

        // Generate cache key from method name and parameters.
        $cache_key = md5(implode('|', [__METHOD__, $use_coupon_param, $coupon_param_source, $user_id]));

        // Return cached value if available and cache is enabled.
        if ($use_cache && isset($this->applicable_coupon_code_cache[$cache_key])) {
            return $this->applicable_coupon_code_cache[$cache_key];
        }

        // First check for coupon in request parameter (if enabled).
        if ($use_coupon_param) {
            $coupon_param = MeprCouponsHelper::validate_coupon_param($this->ID, $coupon_param_source, $user_id);
            if (!empty($coupon_param)) {
                $this->applicable_coupon_code_cache[$cache_key] = $coupon_param;
                return $this->applicable_coupon_code_cache[$cache_key];
            }
        }

        // Get auto-apply coupons (filtered by time applicability).
        $auto_apply_coupons = $this->get_auto_apply_coupons(true, $use_cache);

        // Loop through auto-apply coupons and return the first valid one.
        foreach ($auto_apply_coupons as $coupon_data) {
            $coupon_id = (int) ($coupon_data[MeprGroup::$group_coupon_coupon_id_str] ?? 0);
            if ($coupon_id > 0) {
                $coupon = new MeprCoupon($coupon_id);
                if ($coupon->ID && $coupon->is_valid($this->ID, $user_id, false)) {
                    $this->applicable_coupon_code_cache[$cache_key] = $coupon->post_title; // Coupon code is stored in post_title.
                    return $this->applicable_coupon_code_cache[$cache_key];
                }
            }
        }

        $this->applicable_coupon_code_cache[$cache_key] = '';
        return '';
    }

    /**
     * Get the number of days in the product's period.
     *
     * @param  string $default The default value if no period is set.
     * @return integer|string
     */
    public function days_in_my_period($default = 'lifetime')
    {
        $now            = time();
        $new_expires_at = $this->get_expires_at($now);

        if (is_null($new_expires_at)) {
            return $default;
        }

        $future_date  = new DateTime(gmdate('Y-m-d', $new_expires_at));
        $current_date = new DateTime(gmdate('Y-m-d', $now));
        $timediff     = $current_date->diff($future_date);

        return $timediff->days; // # of days difference
    }

    /**
     * Gets the value for 'expires_at' for the given created_at time for this membership.
     *
     * @param  integer|null $created_at The creation timestamp.
     * @param  boolean      $check_user Whether to check the user for renewals.
     * @return integer|null
     */
    public function get_expires_at($created_at = null, $check_user = true)
    {
        $mepr_options = MeprOptions::fetch();

        if (is_null($created_at)) {
            $created_at = time();
        }

        $user       = MeprUtils::get_currentuserinfo();
        $expires_at = $created_at;
        $period     = $this->period;

        switch ($this->period_type) {
            case 'days':
                $expires_at += MeprUtils::days($period) + MeprUtils::days($mepr_options->grace_expire_days);
                break;
            case 'weeks':
                $expires_at += MeprUtils::weeks($period) + MeprUtils::days($mepr_options->grace_expire_days);
                break;
            case 'months':
                $expires_at += MeprUtils::months($period, $created_at)
                    + MeprUtils::days($mepr_options->grace_expire_days);
                break;
            case 'years':
                $expires_at += MeprUtils::years($period, $created_at)
                    + MeprUtils::days($mepr_options->grace_expire_days);
                break;
            default: // One-time payment.
                if ($this->expire_type === 'delay') {
                    if ($check_user && MeprUtils::is_user_logged_in()) {
                        // Handle renewals.
                        if ($this->is_renewal()) {
                            $last_txn = $this->get_last_active_txn($user->ID);
                            if ($last_txn) {
                                $expires_at = $created_at = strtotime($last_txn->expires_at);
                            }
                        }
                    }

                    switch ($this->expire_unit) {
                        case 'days':
                            $expires_at += MeprUtils::days($this->expire_after);
                            break;
                        case 'weeks':
                            $expires_at += MeprUtils::weeks($this->expire_after);
                            break;
                        case 'months':
                            $expires_at += MeprUtils::months($this->expire_after, $created_at);
                            break;
                        case 'years':
                            $expires_at += MeprUtils::years($this->expire_after, $created_at);
                    }
                } elseif ($this->expire_type === 'fixed') {
                    $expires_at = strtotime($this->expire_fixed);
                    $now        = time();

                    /**
                     * Filter whether to automatically roll over the expiration year
                     * when the fixed expire date is in the past and Allow Early Annual
                     * Renewals is disabled.
                     *
                     * @param bool        $rollover Whether to roll over the year. Default false.
                     * @param MeprProduct $product  The product object.
                     */
                    $rollover_year = MeprHooks::apply_filters(
                        'mepr_fixed_expire_rollover_year',
                        false,
                        $this
                    );

                    // Roll over the year if early renewals are allowed or filter is enabled.
                    if ($this->allow_renewal || $rollover_year) {
                        while ($now > $expires_at) {
                            $expires_at += MeprUtils::years(1);
                        }
                    }
                    // Actually handle renewals.
                    if ($check_user && $this->is_renewal()) {
                        $last_txn = $this->get_last_active_txn($user->ID);
                        if ($last_txn) {
                            $expires_at_date = date_create($last_txn->expires_at, new DateTimeZone('UTC'));

                            if ($expires_at_date instanceof DateTime) {
                                $expires_at_date->modify('+1 year');
                                $expires_at = $expires_at_date->format('U');
                            } else {
                                $expires_at = strtotime($last_txn->expires_at) + MeprUtils::years(1);
                            }
                        }
                    }
                } else { // Lifetime.
                    $expires_at = null;
                }
        }

        return $expires_at;
    }

    /**
     * Get the pricing page product IDs.
     *
     * @return array
     */
    public static function get_pricing_page_product_ids()
    {
        global $wpdb;

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery
        return $wpdb->get_col($wpdb->prepare(
            "SELECT p.ID, p.menu_order
            FROM {$wpdb->postmeta} AS m INNER JOIN {$wpdb->posts} AS p
            ON p.ID = m.post_id
            WHERE m.meta_key = %s
            AND m.meta_value = 1
            ORDER BY p.menu_order, p.ID",
            self::$show_on_pricing_str
        ));
    }

    /**
     * Check if the product is a one-time payment.
     *
     * @return boolean
     */
    public function is_one_time_payment()
    {
        return MeprHooks::apply_filters('mepr_product_is_one_time_payment', $this->period_type === 'lifetime' || (float) $this->price === 0.00);
    }

    /**
     * Check if the membership type is renewable.
     *
     * @return boolean
     */
    public function is_renewable()
    {
        return (($this->expire_type === 'delay' || $this->expire_type === 'fixed') && $this->allow_renewal);
    }

    /**
     * Check if the product is a renewal.
     *
     * @return boolean
     */
    public function is_renewal()
    {
        global $user_ID;

        if (MeprUtils::is_user_logged_in()) {
            $user = new MeprUser($user_ID);
        }

        return (MeprUtils::is_user_logged_in() &&
            $user->is_already_subscribed_to($this->ID) &&
            ($this->expire_type === 'delay' || $this->expire_type === 'fixed') &&
            $this->allow_renewal);
    }

    /**
     * Can this product be purchased?
     *
     * @return boolean
     */
    public function can_you_buy_me()
    {
        $override = MeprHooks::apply_filters('mepr_can_you_buy_me_override', null, $this);
        if (!is_null($override)) {
            return $override;
        }

        // Admins can see & purchase anything.
        if (MeprUtils::is_logged_in_and_an_admin()) {
            return true;
        }

        $user = MeprUtils::get_currentuserinfo();

        // Make sure user hasn't already subscribed to this membership first.
        if (
            $user instanceof MeprUser &&
            $user->is_already_subscribed_to($this->ID) &&
            !$this->simultaneous_subscriptions &&
            !$this->allow_renewal
        ) {
            return false;
        }

        if (empty($this->who_can_purchase)) {
            return true; // No rules exist so everyone can purchase.
        }

        // Do not allow upgrades/downgrades during a pro-rated trial period from a previous upgrade/downgrade.
        if ($user instanceof MeprUser) {
            $group = $this->group();
            if ($group !== false) {
                $sub_in_group = $user->subscription_in_group($group->ID);
                if ($sub_in_group !== false && $sub_in_group->prorated_trial && $sub_in_group->in_trial()) {
                    return MeprHooks::apply_filters('mepr_allow_multiple_upgrades_downgrades', false, $user, $sub_in_group, $this);
                }
            }
        }

        foreach ($this->who_can_purchase as $who) {
            // Give Developers a chance to hook in here
            // Return true or false if you run your own custom handling here
            // Otherwise return string 'no_custom' if MemberPress should handle the processing.
            $custom = MeprHooks::apply_filters('mepr_who_can_purchase_custom_check', 'no_custom', $who, $this);

            if ($custom !== 'no_custom' && is_bool($custom)) {
                return $custom;
            }

            if ($who->user_type === 'disabled') {
                return false;
            }

            if ($who->user_type === 'everyone') {
                return true;
            }

            if ($who->user_type === 'guests' && !MeprUtils::is_user_logged_in()) {
                return true; // If not a logged in member they can purchase.
            }

            if ($who->user_type === 'members' && MeprUtils::is_user_logged_in()) {
                if ($user instanceof MeprUser && $user->can_user_purchase($who, $this->ID)) {
                    return true;
                }
            }
        }

        return false; // If we make it here, nothing applied so let's return false.
    }

    // This is really only used for annual renewals currently
    // It has some special code for fallback transactions (Downgrade Path feature of Groups)
    // So be careful when using this function if you need to includ fallback transactions.
    /**
     * Get the last active transaction for a user.
     *
     * @param  integer $user_id The user ID.
     * @return MeprTransaction|false
     */
    public function get_last_active_txn($user_id)
    {
        global $wpdb;

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery
        $txn_id = $wpdb->get_var(
            $wpdb->prepare(
                "SELECT tr.id AS id FROM {$wpdb->mepr_transactions} AS tr WHERE tr.user_id=%d AND tr.product_id=%d AND tr.status=%s AND tr.expires_at=%s AND tr.txn_type <> %s ORDER BY tr.created_at DESC LIMIT 1",
                $user_id,
                $this->ID,
                MeprTransaction::$complete_str,
                MeprUtils::db_lifetime(),
                MeprTransaction::$fallback_str
            )
        );

        if ($txn_id) {
            // Try for lifetimes.
            return new MeprTransaction($txn_id);
        }

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery
        $txn_id = $wpdb->get_var(
            $wpdb->prepare(
                "SELECT tr.id AS id FROM {$wpdb->mepr_transactions} AS tr WHERE tr.user_id=%d AND tr.product_id=%d AND tr.status=%s AND tr.expires_at > %s ORDER BY tr.created_at DESC LIMIT 1",
                $user_id,
                $this->ID,
                MeprTransaction::$complete_str,
                MeprUtils::db_now()
            )
        );

        if ($txn_id) {
            // Try for expiring.
            return new MeprTransaction($txn_id);
        }

        // TODO: Maybe throw an exception here at some point.
        return false;
    }

    /**
     * Get the group associated with the product.
     *
     * @return MeprGroup|false
     */
    public function group()
    {
        // Don't do static caching stuff here.
        if (!isset($this->group_id) or empty($this->group_id)) {
            return false;
        }

        return new MeprGroup($this->group_id);
    }

    /**
     * Get the URL for the product's group.
     *
     * @return string
     */
    public function group_url()
    {
        $grp = $this->group();
        if ($grp && !$grp->pricing_page_disabled) {
            return $grp->url();
        }

        return $this->url();
    }

    /**
     * Determines if this is a membership upgrade.
     *
     * @return boolean
     */
    public function is_upgrade()
    {
        return $this->is_upgrade_or_downgrade('upgrade');
    }

    /**
     * Determines if this is a membership downgrade.
     *
     * @return boolean
     */
    public function is_downgrade()
    {
        return $this->is_upgrade_or_downgrade('downgrade');
    }

    /**
     * Determines if this is a membership upgrade for a certain user.
     *
     * @param  integer $user_id The user ID.
     * @return boolean
     */
    public function is_upgrade_for($user_id)
    {
        return $this->is_upgrade_or_downgrade_for($user_id, 'upgrade');
    }

    /**
     * Determines if this is a membership downgrade for a certain user.
     *
     * @param  integer $user_id The user ID.
     * @return boolean
     */
    public function is_downgrade_for($user_id)
    {
        return $this->is_upgrade_or_downgrade_for($user_id, 'downgrade');
    }

    /**
     * Check if the product is an upgrade or downgrade.
     *
     * @param  string|false   $type The type of change ('upgrade' or 'downgrade').
     * @param  MeprUser|false $usr  The user object.
     * @return boolean
     */
    public function is_upgrade_or_downgrade($type = false, $usr = false)
    {
        if ($usr === false) {
            $usr = MeprUtils::get_currentuserinfo();
        }
        return ($usr && $this->is_upgrade_or_downgrade_for($usr->ID, $type)); // Must be an upgrade/downgrade for the user.
    }

    /**
     * Determines if this is a membership upgrade or downgrade for a certain user.
     *
     * @param  integer      $user_id The user ID.
     * @param  string|false $type    The type of change ('upgrade' or 'downgrade').
     * @return boolean
     */
    public function is_upgrade_or_downgrade_for($user_id, $type = false)
    {
        $usr = new MeprUser($user_id);
        $grp = $this->group();

        // Not part of a group ... not an upgrade.
        if (!$grp) {
            return false;
        }

        // No upgrade path here ... not an upgrade.
        if (!$grp->is_upgrade_path) {
            return false;
        }

        $prds = $usr->active_product_subscriptions('products', true);

        if (!empty($prds)) {
            foreach ($prds as $p) {
                $g = $p->group();
                if (
                    $g && $g instanceof MeprGroup &&
                    $g->ID === $grp->ID && $this->ID !== $p->ID
                ) {
                    if ($type === false) {
                        return true;
                    } elseif ($type === 'upgrade') {
                        return $this->group_order > $p->group_order;
                    } elseif ($type === 'downgrade') {
                        return $this->group_order < $p->group_order;
                    }
                }
            }
        }

        return false;
    }

    /**
     * Clean up the database for products.
     *
     * @return void
     */
    public static function cleanup_db()
    {
        global $wpdb;
        $date     = time();
        $last_run = get_option(self::$last_run_str, 0); // Prevents all this code from executing on every page load.

        if (($date - $last_run) > 86400) { // Runs once at most once a day.
            update_option(self::$last_run_str, $date);
            $sq1_res = $wpdb->get_col($wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
                "SELECT ID
                FROM {$wpdb->posts}
                WHERE post_type = %s AND
                      post_status = 'auto-draft'",
                self::$cpt
            ));
            if (!empty($sq1_res)) {
                $post_ids = implode(',', $sq1_res);
                $q        = "DELETE
                  FROM {$wpdb->postmeta}
                  WHERE post_id IN ({$post_ids})";
                $wpdb->query($q); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery, PluginCheck.Security.DirectDB.UnescapedDBParameter
                $wpdb->query($wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
                    "DELETE FROM {$wpdb->posts}
                     WHERE post_type = %s AND
                           post_status = 'auto-draft'",
                    self::$cpt
                ));
            }
        }
    }

    /**
     * Get the page template for the product.
     *
     * @return string|null
     */
    public function get_page_template()
    {
        if ($this->use_custom_template) {
            return locate_template($this->custom_template);
        }

        return null;
    }

    /*
     * Defines the template hierarchy search path for member core products.
     * Currently unused method that would specify template file lookup order.
     *
        public static function template_search_path() {
            return array( 'page_memberpressproduct.php',
                      'single-memberpressproduct.php',
                      'page.php',
                      'custom_template.php',
                      'single.php',
                      'index.php' );
        }
     */

    /**
     * Get the payment methods for the product.
     *
     * @return array
     */
    public function payment_methods()
    {
        $mepr_options = MeprOptions::fetch();

        $pms = $mepr_options->payment_methods();

        unset($pms['free']);
        unset($pms['manual']);

        $pmkeys = array_keys($pms);

        if (
            isset($this->custom_payment_methods) and
            !is_null($this->custom_payment_methods) and
            is_array($this->custom_payment_methods)
        ) {
            return array_intersect($this->custom_payment_methods, $pmkeys);
        }

        return $pmkeys;
    }

    /**
     * Get the edit URL for the product.
     *
     * @param  string $args Additional URL arguments.
     * @return string
     */
    public function edit_url($args = '')
    {
        if (isset($this->ID) && $this->post_type === self::$cpt) {
            return get_edit_post_link($this->ID);
        } else {
            return '';
        }
    }

    /**
     * Get the URL for the product.
     *
     * @param  string  $args            Additional URL arguments.
     * @param  boolean $modify_if_https Whether to modify the URL if HTTPS is used.
     * @return string
     */
    public function url($args = '', $modify_if_https = false)
    {
        if (isset($this->ID)) {
            $url = MeprUtils::get_permalink($this->ID) . $args;

            if (MeprUtils::is_ssl() && $modify_if_https) {
                $url = preg_replace('!^http:!', 'https:', $url);
            }
            $url = MeprHooks::apply_filters('mepr_product_url', $url, $this, $args, $modify_if_https);

            return $url;
        } else {
            return '';
        }
    }

    /**
     * Check if the product has a manual signup form.
     *
     * @return boolean
     */
    public function manual_append_signup()
    {
        return preg_match('~\[\s*mepr[-_](product|membership)[-_]registration[-_]form\s*\]~', $this->post_content);
    }

    /**
     * Get the custom profile fields for the product.
     *
     * @return array
     */
    public function custom_profile_fields()
    {
        $mepr_options = MeprOptions::fetch();
        $fields       = [];

        if (!$this->customize_profile_fields) {
            return $mepr_options->custom_fields;
        }

        foreach ($mepr_options->custom_fields as $row) {
            if (in_array($row->field_key, $this->custom_profile_fields, true)) {
                $fields[] = $row;
            }
        }

        return $fields;
    }

    /**
     * This function is to be used to determine if a trial should be allowed.
     * The current idea here is that if the user has
     *
     * @return boolean
     */
    public function trial_is_expired()
    {
        global $wpdb;

        if ($this->trial && MeprUtils::is_user_logged_in()) {
            $current_user = MeprUtils::get_currentuserinfo();
            if ($current_user) {
                // phpcs:ignore WordPress.DB.DirectDatabaseQuery
                $already_trialled = $wpdb->get_var(
                    $wpdb->prepare(
                        "SELECT COUNT(*) FROM {$wpdb->mepr_transactions} AS t WHERE t.user_id=%d AND t.product_id=%d AND t.txn_type IN (%s,%s) AND t.status IN (%s,%s,%s)",
                        $current_user->ID,
                        $this->ID,
                        MeprTransaction::$subscription_confirmation_str,
                        MeprTransaction::$payment_str,
                        MeprTransaction::$complete_str,
                        MeprTransaction::$refunded_str,
                        MeprTransaction::$confirmed_str
                    )
                );

                return ($already_trialled > 0);
            }
        }

        return false;
    }

    /**
     * Check if a post is a product page.
     *
     * @param  WP_Post $post The post object.
     * @return MeprProduct|false
     */
    public static function is_product_page($post)
    {
        $return = false;

        if (is_object($post) && property_exists($post, 'post_type') && $post->post_type === MeprProduct::$cpt) {
            $prd    = new MeprProduct($post->ID);
            $return = $prd;
        } elseif (
            is_object($post) && preg_match(
                '~\[(mepr_(product|membership)_registration_form|mepr-(product|membership)-registration-form)\s+(product_)?id=[\"\\\'](\d+)[\"\\\']~',
                $post->post_content,
                $m
            )
        ) {
            if (isset($m[5])) {
                $prd    = new MeprProduct($m[5]);
                $return = $prd;
            }
        }

        return MeprHooks::apply_filters('mepr_is_product_page', $return, $post);
    }

    /**
     * Get the highest menu order active membership by user.
     *
     * @param  integer $user_id The user ID.
     * @return integer|false
     */
    public static function get_highest_menu_order_active_membership_by_user($user_id)
    {
        global $wpdb;

        $user = new MeprUser($user_id);

        if ((int)$user->ID === 0) {
            return false;
        }

        $active_memberships = array_unique($user->active_product_subscriptions('ids'), true);
        $active_memberships = array_map('absint', $active_memberships);

        if (empty($active_memberships)) {
            return false;
        }

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery
        $result = $wpdb->get_var(
            $wpdb->prepare(
                "SELECT ID FROM {$wpdb->posts}
                WHERE FIND_IN_SET(ID, %s)
                ORDER BY menu_order DESC, ID DESC LIMIT 1",
                implode(',', $active_memberships)
            )
        );

        return (!is_null($result)) ? (int) $result : false;
    }

    /**
     * Get the Stripe Product ID
     *
     * @param  string $gateway_id The gateway ID.
     * @return string|false
     */
    public function get_stripe_product_id($gateway_id)
    {
        return get_post_meta($this->ID, '_mepr_stripe_product_id_' . $gateway_id, true);
    }

    /**
     * Get the Stripe Initial payment product ID
     *
     * @param  string $gateway_id The gateway ID.
     * @return string|false
     */
    public function get_stripe_initial_payment_product_id($gateway_id)
    {
        return get_post_meta($this->ID, '_mepr_stripe_initial_payment_product_id_' . $gateway_id, true);
    }

    /**
     * Set the Stripe Product ID
     *
     * @param string $gateway_id The gateway ID.
     * @param string $product_id The Stripe Product ID.
     */
    public function set_stripe_product_id($gateway_id, $product_id)
    {
        update_post_meta($this->ID, '_mepr_stripe_product_id_' . $gateway_id, $product_id);
    }

    /**
     * Set the Stripe Initial Payment Product ID
     *
     * @param string $gateway_id The gateway ID.
     * @param string $product_id The Stripe Product ID.
     */
    public function set_stripe_initial_payment_product_id($gateway_id, $product_id)
    {
        update_post_meta($this->ID, '_mepr_stripe_initial_payment_product_id_' . $gateway_id, $product_id);
    }

    /**
     * Get the Stripe Plan ID for this product's current payment terms
     *
     * @param  string $gateway_id The gateway ID.
     * @param  string $amount     The payment amount (excluding tax).
     * @return string|false
     */
    public function get_stripe_plan_id($gateway_id, $amount)
    {
        $meta_key = sprintf('_mepr_stripe_plan_id_%s_%s', $gateway_id, $this->terms_hash($amount));

        $plan_id = get_post_meta($this->ID, $meta_key, true);

        return MeprHooks::apply_filters('mepr_product_get_stripe_plan_id', $plan_id, $this, $gateway_id, $amount);
    }

    /**
     * Set the Stripe Plan ID for this product's current payment terms
     *
     * @param string $gateway_id The gateway ID.
     * @param string $amount     The payment amount (excluding tax).
     * @param string $plan_id    The Stripe Plan ID.
     */
    public function set_stripe_plan_id($gateway_id, $amount, $plan_id)
    {
        $meta_key = sprintf('_mepr_stripe_plan_id_%s_%s', $gateway_id, $this->terms_hash($amount));

        update_post_meta($this->ID, $meta_key, $plan_id);
    }

    /**
     * Get the hash of the payment terms
     *
     * If this hash changes then a different Stripe Plan will be created.
     *
     * @param  string $amount The payment amount.
     * @return string
     */
    private function terms_hash($amount)
    {
        $mepr_options = MeprOptions::fetch();

        $terms = [
            'currency'    => $mepr_options->currency_code,
            'amount'      => $amount,
            'period'      => $this->period,
            'period_type' => $this->period_type,
        ];

        return md5(serialize($terms));
    }

    /**
     * Delete the Stripe Product ID
     *
     * @param string $gateway_id The gateway ID.
     * @param string $product_id The Stripe Product ID.
     */
    public static function delete_stripe_product_id($gateway_id, $product_id)
    {
        if (is_string($product_id) && $product_id !== '') {
            delete_metadata('post', null, '_mepr_stripe_product_id_' . $gateway_id, $product_id, true);
        }
    }

    /**
     * Delete the Stripe Plan ID
     *
     * @param string $gateway_id The gateway ID.
     * @param string $plan_id    The Stripe Plan ID.
     */
    public static function delete_stripe_plan_id($gateway_id, $plan_id)
    {
        if (!is_string($plan_id) || $plan_id === '') {
            return;
        }

        global $wpdb;

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery
        $meta_ids = $wpdb->get_col($wpdb->prepare(
            "SELECT meta_id FROM {$wpdb->postmeta} WHERE meta_key LIKE %s AND meta_value = %s",
            $wpdb->esc_like('_mepr_stripe_plan_id_' . $gateway_id) . '%',
            $plan_id
        ));

        if (is_array($meta_ids) && count($meta_ids)) {
            foreach ($meta_ids as $meta_id) {
                delete_metadata_by_mid('post', $meta_id);
            }
        }
    }

    /**
     * Get the array of order bumps chosen for this product
     *
     * @return MeprProduct[]
     */
    public function get_order_bumps()
    {
        $product_ids = get_post_meta($this->ID, '_mepr_order_bumps', true);
        $order_bumps = [];

        if (is_array($product_ids)) {
            foreach ($product_ids as $product_id) {
                $product = new MeprProduct((int) $product_id);

                if ($product->ID > 0) {
                    $order_bumps[] = $product;
                }
            }
        }

        return $order_bumps;
    }

    /**
     * Get the array of order bumps chosen for this product marked as required.
     *
     * @return int[]
     */
    public function get_required_order_bumps()
    {
        $product_ids = get_post_meta($this->ID, '_mepr_order_bumps_required', true);
        $order_bumps = [];
        if (!is_array($product_ids)) {
            return $order_bumps;
        }

        foreach ($product_ids as $product_id) {
            $product = new MeprProduct((int) $product_id);
            if ($product->ID > 0) {
                $order_bumps[] = $product->ID;
            }
        }

        return $order_bumps;
    }
}
