<?php

if ( !defined( 'ABSPATH' ) ) {
    exit;
}
class BSPP_Plugin {
    private $magic_login;

    private $email_health;

    /** @var string[] Hook suffixes for plugin admin pages (from add_menu_page / add_submenu_page). */
    private $admin_page_hooks = [];

    public function __construct() {
        $this->magic_login = new BSPP_Magic_Login();
        $this->email_health = new BSPP_Email_Health();
        $this->init_hooks();
    }

    private function init_hooks() {
        add_action( 'init', [$this, 'init'] );
        add_action( 'admin_menu', [$this, 'register_admin_menu'] );
        add_action( 'admin_enqueue_scripts', [$this, 'enqueue_admin_page_assets'] );
        add_action( 'admin_enqueue_scripts', [$this, 'enqueue_admin_menu_styles'] );
        add_action( 'admin_enqueue_scripts', [$this, 'maybe_style_onboarding_submenu'] );
        add_action( 'admin_init', [$this, 'maybe_redirect_to_onboarding'], 1 );
        add_action( 'admin_notices', [$this, 'maybe_show_onboarding_notice'] );
        add_action( 'template_redirect', [$this, 'maybe_fulfill_stripe_return'], 5 );
        add_action( 'wp_enqueue_scripts', [$this, 'maybe_enqueue_paywall_overlay_assets'], 20 );
        add_action( 'wp_enqueue_scripts', [$this, 'maybe_enqueue_manage_subscription_note_assets'], 20 );
        add_filter( 'the_content', [$this, 'maybe_apply_paywall'] );
        add_filter( 'the_content', [$this, 'maybe_append_manage_subscription_after_content'], 20 );
        add_action( 'admin_post_bspp_start_subscription', [$this, 'handle_start_subscription'] );
        add_action( 'admin_post_nopriv_bspp_start_subscription', [$this, 'handle_start_subscription'] );
        add_action( 'admin_post_bspp_save_access_monetization', [$this, 'handle_save_access_monetization'] );
        add_action( 'admin_post_bspp_save_stripe_settings', [$this, 'handle_save_stripe_settings'] );
        add_action( 'admin_post_bspp_send_test_email', [$this, 'handle_send_test_email'] );
        add_action( 'admin_post_bspp_stripe_webhook', [$this, 'handle_stripe_webhook'] );
        add_action( 'admin_post_nopriv_bspp_stripe_webhook', [$this, 'handle_stripe_webhook'] );
        // Guided onboarding wizard (Phase 9/10 — creator setup flow).
        add_action( 'admin_post_bspp_onboarding_step1_save', [$this, 'handle_onboarding_step1_save'] );
        add_action( 'admin_post_bspp_onboarding_step2_save', [$this, 'handle_onboarding_step2_save'] );
        add_action( 'admin_post_bspp_onboarding_step3_save', [$this, 'handle_onboarding_step3_save'] );
        add_action( 'admin_post_bspp_onboarding_launch', [$this, 'handle_onboarding_launch'] );
        add_action( 'admin_post_bspp_onboarding_reset', [$this, 'handle_onboarding_reset'] );
        add_action( 'admin_post_bspp_stripe_customer_portal', [$this, 'handle_stripe_customer_portal'] );
        add_action( 'admin_post_bspp_cancel_membership', [$this, 'handle_cancel_membership'] );
        add_action( 'admin_post_bspp_refund_purchase', [$this, 'handle_refund_purchase'] );
        add_filter(
            'wp_nav_menu_items',
            [$this, 'append_manage_subscription_footer_menu_item'],
            10,
            2
        );
        add_filter(
            'pre_render_block',
            [$this, 'track_footer_template_part_pre_render'],
            10,
            2
        );
        add_filter(
            'render_block',
            [$this, 'track_footer_template_part_render_end'],
            9,
            2
        );
        add_filter(
            'render_block',
            [$this, 'maybe_append_manage_subscription_block_navigation'],
            10,
            2
        );
        add_filter( 'allowed_redirect_hosts', [$this, 'allow_stripe_redirect_hosts'] );
    }

    public function init() {
        // Placeholder for future initialization
    }

    /**
     * Allow redirects to Stripe Checkout and Billing Portal hosts.
     *
     * @param string[] $hosts Allowed redirect hosts.
     * @return string[]
     */
    public function allow_stripe_redirect_hosts( $hosts ) {
        $hosts[] = 'checkout.stripe.com';
        $hosts[] = 'billing.stripe.com';
        return $hosts;
    }

    /**
     * Enqueue paywall overlay styles on the Tools admin preview page.
     *
     * @param string $hook_suffix Current admin page hook suffix.
     */
    public function enqueue_admin_tools_assets( $hook_suffix ) {
    }

    public function maybe_enqueue_paywall_overlay_assets() {
        if ( is_admin() || !is_singular( 'post' ) ) {
            return;
        }
        $post_id = get_queried_object_id();
        if ( $post_id <= 0 ) {
            return;
        }
        if ( !class_exists( 'BSPP_Paywall_Overlay' ) ) {
            return;
        }
        if ( !BSPP_Paywall_Overlay::is_post_locked_for_visitor( $post_id ) ) {
            return;
        }
        BSPP_Paywall_Overlay::enqueue_assets();
    }

    /**
     * Overlay stylesheet also styles the subscriber manage-subscription note.
     */
    public function maybe_enqueue_manage_subscription_note_assets() {
        if ( is_admin() || !is_singular( 'post' ) ) {
            return;
        }
        if ( !class_exists( 'BSPP_Paywall_Overlay' ) ) {
            return;
        }
        if ( self::get_manage_subscription_portal_url_for_user() === '' ) {
            return;
        }
        BSPP_Paywall_Overlay::enqueue_assets();
    }

    /**
     * When Stripe redirects back with session_id, fulfill if paid (webhooks often miss localhost).
     */
    public function maybe_fulfill_stripe_return() {
        if ( is_admin() || wp_doing_ajax() ) {
            return;
        }
        if ( !is_singular( 'post' ) ) {
            return;
        }
        $user_id = get_current_user_id();
        if ( $user_id <= 0 ) {
            return;
        }
        if ( empty( $_GET['session_id'] ) || !is_string( $_GET['session_id'] ) ) {
            return;
        }
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- not a form submission, Stripe return URL parameter.
        $session_id = sanitize_text_field( wp_unslash( $_GET['session_id'] ) );
        if ( $session_id === '' || strpos( $session_id, 'cs_' ) !== 0 ) {
            return;
        }
        $flow = null;
        if ( $flow === null && isset( $_GET['bspp_stripe'] ) && sanitize_key( wp_unslash( $_GET['bspp_stripe'] ) ) === 'success' ) {
            $flow = 'subscription';
        }
        // phpcs:enable WordPress.Security.NonceVerification.Recommended
        if ( $flow === null ) {
            return;
        }
        $post_id = get_queried_object_id();
        if ( $post_id <= 0 ) {
            return;
        }
        $cache_key = 'bobasoftware_pp_return_ok_' . md5( $session_id . '_' . (string) $user_id );
        if ( get_transient( $cache_key ) ) {
            return;
        }
        $checkout = new BSPP_Stripe_Checkout();
        $session = $checkout->retrieve_checkout_session( $session_id );
        if ( is_wp_error( $session ) ) {
            return;
        }
        if ( empty( $session['id'] ) || $session['id'] !== $session_id ) {
            return;
        }
        $metadata = ( is_array( $session['metadata'] ?? null ) ? $session['metadata'] : [] );
        $meta_uid = ( isset( $metadata['user_id'] ) ? (int) $metadata['user_id'] : 0 );
        $meta_post = ( isset( $metadata['post_id'] ) ? (int) $metadata['post_id'] : 0 );
        $purchase_type = ( isset( $metadata['purchase_type'] ) ? sanitize_key( $metadata['purchase_type'] ) : '' );
        if ( $meta_uid !== $user_id || $meta_post !== $post_id ) {
            return;
        }
        if ( $flow === 'tip_unlock' && $purchase_type !== 'tip_unlock' ) {
            return;
        }
        if ( $flow === 'article_unlock' && $purchase_type !== 'article_unlock' ) {
            return;
        }
        if ( $flow === 'subscription' && $purchase_type !== 'subscription' ) {
            return;
        }
        $mode = ( isset( $session['mode'] ) ? (string) $session['mode'] : '' );
        $payment_status = ( isset( $session['payment_status'] ) ? (string) $session['payment_status'] : '' );
        if ( $mode === 'payment' && $payment_status !== 'paid' ) {
            return;
        }
        if ( $mode === 'subscription' && !in_array( $payment_status, ['paid', 'no_payment_required'], true ) ) {
            return;
        }
        if ( $mode !== 'payment' && $mode !== 'subscription' ) {
            return;
        }
        BSPP_Stripe_Webhook::fulfill_checkout_session_completed( $session );
        set_transient( $cache_key, 1, HOUR_IN_SECONDS );
    }

    public function handle_unlock_article() {
        wp_die( 'Per-article unlock is not available in this build.' );
    }

    public function handle_use_free_post() {
        wp_die( 'Free quota is not available in this build.' );
    }

    public function handle_start_subscription() {
        $post_id = ( isset( $_REQUEST['post_id'] ) ? (int) $_REQUEST['post_id'] : 0 );
        $user_id = get_current_user_id();
        $requested_plan = ( isset( $_REQUEST['subscription_plan'] ) ? sanitize_key( wp_unslash( $_REQUEST['subscription_plan'] ) ) : 'monthly' );
        if ( !isset( $_REQUEST['bspp_subscribe_nonce'] ) || !wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['bspp_subscribe_nonce'] ) ), 'bspp_start_subscription' ) ) {
            wp_die( 'Invalid request' );
        }
        if ( !$post_id || !get_post( $post_id ) ) {
            wp_die( 'Invalid post' );
        }
        $settings = new BSPP_Settings_Repository();
        $monthly_price = (float) $settings->get( 'subscription_price_monthly', 9.99 );
        $annual_price = (float) $settings->get( 'subscription_price_annual', 99 );
        $is_annual = $requested_plan === 'annual';
        $subscription_price = ( $is_annual ? $annual_price : $monthly_price );
        $interval = ( $is_annual ? 'year' : 'month' );
        $plan = ( $is_annual ? 'annual' : 'monthly' );
        // If Stripe is not configured yet, show a friendly message.
        $stripe_api_key = $settings->get( 'stripe_api_key', '' );
        if ( empty( $stripe_api_key ) ) {
            $redirect_url = add_query_arg( 'bspp_subscribe', 'pending', get_permalink( $post_id ) );
            wp_safe_redirect( $redirect_url );
            exit;
        }
        $checkout_email = '';
        if ( $user_id > 0 ) {
            $user = get_user_by( 'id', $user_id );
            if ( $user && !empty( $user->user_email ) ) {
                $checkout_email = sanitize_email( (string) $user->user_email );
            }
        }
        $purchase_repo = new BSPP_Purchase_Repository();
        $purchase_type = ( $is_annual ? 'subscription_annual' : 'subscription_monthly' );
        $pending_user_id = ( $user_id > 0 ? $user_id : null );
        $pending_email = ( $checkout_email !== '' ? $checkout_email : null );
        $purchase_id = $purchase_repo->create_pending(
            $pending_user_id,
            $pending_email,
            $purchase_type,
            $subscription_price,
            'pending'
        );
        if ( !$purchase_id ) {
            wp_die( 'Could not create purchase.' );
        }
        $checkout = new BSPP_Stripe_Checkout();
        $checkout_result = $checkout->create_subscription_checkout_session(
            (int) $user_id,
            $checkout_email,
            $purchase_id,
            $subscription_price,
            $interval,
            $post_id,
            $plan
        );
        if ( is_wp_error( $checkout_result ) ) {
            $error_code = $checkout_result->get_error_code();
            $error_message = $checkout_result->get_error_message();
            wp_die( 'Could not start Stripe checkout: ' . esc_html( $error_code ) . ' - ' . esc_html( $error_message ) );
        }
        $purchase_repo->update_stripe_session_id( $purchase_id, $checkout_result['stripe_session_id'] );
        // Allow redirects to Stripe Checkout domain explicitly for MVP.
        $redirect_url = $checkout_result['stripe_session_url'];
        $redirect_host = wp_parse_url( $redirect_url, PHP_URL_HOST );
        if ( $redirect_host === 'checkout.stripe.com' ) {
            wp_safe_redirect( $redirect_url );
            exit;
        }
        wp_safe_redirect( get_permalink( $post_id ) );
        exit;
    }

    public function handle_save_stripe_settings() {
        if ( !current_user_can( 'manage_options' ) ) {
            wp_die( 'Unauthorized' );
        }
        if ( !isset( $_POST['bspp_stripe_settings_nonce'] ) || !wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['bspp_stripe_settings_nonce'] ) ), 'bspp_save_stripe_settings' ) ) {
            wp_die( 'Invalid request' );
        }
        $stripe_api_key = ( isset( $_POST['stripe_api_key'] ) ? sanitize_text_field( wp_unslash( $_POST['stripe_api_key'] ) ) : '' );
        $stripe_webhook_secret = ( isset( $_POST['stripe_webhook_secret'] ) ? sanitize_text_field( wp_unslash( $_POST['stripe_webhook_secret'] ) ) : '' );
        $settings = new BSPP_Settings_Repository();
        $settings->set( 'stripe_api_key', $stripe_api_key );
        $settings->set( 'stripe_webhook_secret', $stripe_webhook_secret );
        wp_safe_redirect( admin_url( 'admin.php?page=bobasoftware-publisher-paywall-settings&bspp_settings=saved' ) );
        exit;
    }

    public function handle_send_test_email() {
        if ( !current_user_can( 'manage_options' ) ) {
            wp_die( 'Unauthorized' );
        }
        if ( !isset( $_POST['bspp_email_test_nonce'] ) || !wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['bspp_email_test_nonce'] ) ), 'bspp_send_test_email' ) ) {
            wp_die( 'Invalid request' );
        }
        $user = wp_get_current_user();
        $result = $this->email_health->bspp_send_test_email( $user->user_email );
        if ( $result === true ) {
            wp_safe_redirect( $this->bspp_email_test_redirect_url( 'success' ) );
            exit;
        }
        wp_safe_redirect( $this->bspp_email_test_redirect_url( 'failed' ) );
        exit;
    }

    /**
     * Redirect target after "Send test email" (Settings or onboarding step 3).
     *
     * @param string $result success|failed
     */
    private function bspp_email_test_redirect_url( $result ) {
        $return_to = ( isset( $_POST['bspp_email_return_to'] ) ? sanitize_key( wp_unslash( (string) $_POST['bspp_email_return_to'] ) ) : 'settings' );
        if ( $return_to === 'onboarding' ) {
            return admin_url( 'admin.php?page=bobasoftware-publisher-paywall-onboarding&bspp_onboarding_step=3&bspp_email_test=' . $result );
        }
        return admin_url( 'admin.php?page=bobasoftware-publisher-paywall-settings&bspp_email_test=' . $result );
    }

    public function handle_save_access_monetization() {
        if ( !current_user_can( 'manage_options' ) ) {
            wp_die( 'Unauthorized' );
        }
        if ( !isset( $_POST['bspp_access_monetization_nonce'] ) || !wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['bspp_access_monetization_nonce'] ) ), 'bspp_save_access_monetization' ) ) {
            wp_die( 'Invalid request' );
        }
        $settings = new BSPP_Settings_Repository();
        $d = BSPP_Settings_Repository::defaults();
        $enable_subscription = ( isset( $_POST['enable_subscription'] ) ? 1 : 0 );
        // Disabled inputs are not submitted — keep existing DB values (or repo defaults if missing).
        $subscription_price_monthly = ( isset( $_POST['subscription_price_monthly'] ) ? floatval( wp_unslash( $_POST['subscription_price_monthly'] ) ) : (float) $settings->get( 'subscription_price_monthly', $d['subscription_price_monthly'] ) );
        $subscription_price_annual = ( isset( $_POST['subscription_price_annual'] ) ? floatval( wp_unslash( $_POST['subscription_price_annual'] ) ) : (float) $settings->get( 'subscription_price_annual', $d['subscription_price_annual'] ) );
        $posts_become_paid_after_days = ( isset( $_POST['posts_become_paid_after_days'] ) ? absint( wp_unslash( $_POST['posts_become_paid_after_days'] ) ) : (int) $settings->get( 'posts_become_paid_after_days', $d['posts_become_paid_after_days'] ) );
        $subscription_price_monthly = max( 0, $subscription_price_monthly );
        $subscription_price_annual = max( 0, $subscription_price_annual );
        $posts_become_paid_after_days = max( 0, $posts_become_paid_after_days );
        $settings->set( 'enable_subscription', $enable_subscription );
        $settings->set( 'subscription_price_monthly', $subscription_price_monthly );
        $settings->set( 'subscription_price_annual', $subscription_price_annual );
        $settings->set( 'posts_become_paid_after_days', $posts_become_paid_after_days );
        wp_safe_redirect( admin_url( 'admin.php?page=bobasoftware-publisher-paywall&bspp_access_monetization=saved' ) );
        exit;
    }

    public function handle_start_tip_unlock() {
        wp_die( 'Tip unlock is not available in this build.' );
    }

    public function handle_stripe_webhook() {
        $webhook = new BSPP_Stripe_Webhook();
        $webhook->handle();
    }

    public function register_admin_menu() {
        $this->admin_page_hooks = [];
        $this->admin_page_hooks[] = add_menu_page(
            'Access & Monetization',
            'Publisher Paywall',
            'manage_options',
            'bobasoftware-publisher-paywall',
            [$this, 'render_access_monetization_page'],
            'dashicons-lock',
            65
        );
        // First submenu: same slug as parent so WP uses this label instead of duplicating the parent title.
        $this->admin_page_hooks[] = add_submenu_page(
            'bobasoftware-publisher-paywall',
            'Access & Monetization',
            'Access & Monetization',
            'manage_options',
            'bobasoftware-publisher-paywall',
            [$this, 'render_access_monetization_page']
        );
        $this->admin_page_hooks[] = add_submenu_page(
            'bobasoftware-publisher-paywall',
            'Members',
            'Members',
            'manage_options',
            'bobasoftware-publisher-paywall-members',
            [$this, 'render_members_admin_page']
        );
        $this->admin_page_hooks[] = add_submenu_page(
            'bobasoftware-publisher-paywall',
            'Payments',
            'Payments',
            'manage_options',
            'bobasoftware-publisher-paywall-payments',
            [$this, 'render_payments_admin_page']
        );
        $this->admin_page_hooks[] = add_submenu_page(
            'bobasoftware-publisher-paywall',
            'Settings',
            'Settings',
            'manage_options',
            'bobasoftware-publisher-paywall-settings',
            [$this, 'render_settings_page']
        );
        $this->admin_page_hooks[] = add_submenu_page(
            'bobasoftware-publisher-paywall',
            'Help',
            'Help',
            'manage_options',
            'bobasoftware-publisher-paywall-help',
            [$this, 'render_help_admin_page']
        );
        // Last core item: optional return / testing; primary work happens under Access & Monetization after launch.
        $this->admin_page_hooks[] = add_submenu_page(
            'bobasoftware-publisher-paywall',
            'Setup wizard',
            'Setup wizard',
            'manage_options',
            'bobasoftware-publisher-paywall-onboarding',
            [$this, 'render_onboarding_page']
        );
        $this->admin_page_hooks[] = add_submenu_page(
            'bobasoftware-publisher-paywall',
            'Upgrade to Pro',
            'Upgrade to Pro',
            'manage_options',
            'bobasoftware-publisher-paywall-upgrade',
            [$this, 'render_upgrade_to_pro_page']
        );
        $this->admin_page_hooks = array_values( array_filter( $this->admin_page_hooks ) );
    }

    /**
     * Enqueue shared admin CSS/JS on plugin admin pages only.
     *
     * @param string $hook_suffix Current admin page hook suffix.
     */
    public function enqueue_admin_page_assets( $hook_suffix ) {
        if ( !is_string( $hook_suffix ) || $hook_suffix === '' ) {
            return;
        }
        if ( empty( $this->admin_page_hooks ) || !in_array( $hook_suffix, $this->admin_page_hooks, true ) ) {
            return;
        }
        $base = ( defined( 'BSPP_PLUGIN_URL' ) ? BSPP_PLUGIN_URL : plugin_dir_url( dirname( __FILE__ ) ) );
        wp_enqueue_style(
            'bspp-admin-styles',
            $base . 'admin/css/bspp-admin-styles.css',
            array(),
            BSPP_VERSION
        );
        wp_enqueue_script(
            'bspp-admin-scripts',
            $base . 'admin/js/bspp-admin-scripts.js',
            array(),
            BSPP_VERSION,
            true
        );
    }

    /**
     * Free-build admin UI styles: Upgrade menu accent, Access & Monetization upsell, Payments banner.
     *
     * @param string $hook_suffix Current admin page hook suffix.
     */
    public function enqueue_admin_menu_styles( $hook_suffix ) {
        wp_register_style(
            'bspp-admin-menu',
            false,
            array(),
            BSPP_VERSION
        );
        wp_enqueue_style( 'bspp-admin-menu' );
        wp_add_inline_style( 'bspp-admin-menu', '#adminmenu .toplevel_page_bobasoftware-publisher-paywall a[href*="bobasoftware-publisher-paywall-upgrade"]{color:#22C55E !important;font-weight:600;}' );
        if ( $hook_suffix === 'toplevel_page_bobasoftware-publisher-paywall' ) {
            wp_add_inline_style( 'bspp-admin-menu', '.sp-am-pro-upsell{margin-top:28px;padding:32px;background:#FFFFFF;border:1px solid #E5E7EB;border-radius:11px;box-shadow:0 1px 2px rgba(0,0,0,0.04);}' . '.sp-am-pro-upsell__title{margin:0 0 8px;font-size:22px;font-weight:600;letter-spacing:-0.2px;line-height:1.45;color:#1d2327;}' . '.sp-am-pro-upsell__lede{margin:0 0 20px;font-size:14px;line-height:1.5;color:#6B7280;}' . '.sp-am-pro-upsell__cards{display:grid;grid-template-columns:repeat(3,1fr);gap:16px;margin:0 0 24px;}' . '@media (max-width:782px){.sp-am-pro-upsell__cards{grid-template-columns:1fr;}}' . '.sp-am-pro-upsell__card{background:#F9FAFB;border:1px solid #E5E7EB;border-radius:10px;padding:20px;}' . '.sp-am-pro-upsell__card-title{margin:0 0 8px;font-size:18px;font-weight:500;line-height:1.45;color:#111827;}' . '.sp-am-pro-upsell__card-body{margin:0;font-size:16px;font-weight:400;line-height:1.5;color:#6B7280;}' . '.sp-am-pro-upsell__actions{display:flex;flex-wrap:wrap;gap:14px;align-items:center;}' . '.sp-am-pro-upsell__cta{display:inline-block;padding:12px 20px;border-radius:8px;background:#4F46E5;color:#FFFFFF !important;font-size:14px;font-weight:600;line-height:1.4;text-decoration:none;}' . '.sp-am-pro-upsell__cta:hover,.sp-am-pro-upsell__cta:focus{background:#4338CA;color:#FFFFFF !important;}' . '.sp-am-pro-upsell__learn{color:#6B7280;font-size:14px;font-weight:500;text-decoration:none;}' . '.sp-am-pro-upsell__learn:hover,.sp-am-pro-upsell__learn:focus{color:#4F46E5;text-decoration:underline;}' );
        }
        if ( $hook_suffix === 'bobasoftware-publisher-paywall_page_bobasoftware-publisher-paywall-payments' || $hook_suffix === 'publisher-paywall_page_bobasoftware-publisher-paywall-payments' || is_string( $hook_suffix ) && strpos( $hook_suffix, 'bobasoftware-publisher-paywall-payments' ) !== false ) {
            // Attach to core `common` so the inline CSS always prints in wp-admin.
            wp_enqueue_style( 'common' );
            wp_add_inline_style( 'common', '.bspp-payments-upsell-banner{display:flex;flex-wrap:wrap;gap:8px 16px;align-items:center;margin:0 0 24px;padding:16px 24px;background:#EEF2FF;border-radius:6px;font-size:15px;line-height:1.5;color:#374151;}' . '.bspp-payments-upsell-banner p{margin:0;font-size:15px;line-height:1.5;color:#374151;}' . '.bspp-payments-upsell-banner a{color:#4F46E5;font-size:15px;font-weight:600;text-decoration:none;white-space:nowrap;}' . '.bspp-payments-upsell-banner a:hover,.bspp-payments-upsell-banner a:focus{color:#4338CA;text-decoration:underline;}' );
        }
        if ( $hook_suffix === 'bobasoftware-publisher-paywall_page_bobasoftware-publisher-paywall-onboarding' || $hook_suffix === 'publisher-paywall_page_bobasoftware-publisher-paywall-onboarding' || is_string( $hook_suffix ) && strpos( $hook_suffix, 'bobasoftware-publisher-paywall-onboarding' ) !== false ) {
            wp_add_inline_style( 'bspp-admin-menu', '.sp-ob-pro-callout{margin-top:16px;padding:16px;background:#EEF2FF;border:1px solid #E0E7FF;border-radius:10px;max-width:640px;}' . '.sp-ob-pro-callout__title{margin:0 0 6px;font-size:16px;font-weight:600;line-height:1.45;color:#111827;}' . '.sp-ob-pro-callout__body{margin:0 0 10px;font-size:14px;font-weight:400;line-height:1.5;color:#6B7280;}' . '.sp-ob-pro-callout__link{color:#4F46E5;font-size:13px;font-weight:600;text-decoration:none;}' . '.sp-ob-pro-callout__link:hover,.sp-ob-pro-callout__link:focus{color:#4338CA;text-decoration:underline;}' . '.sp-onboarding-actions--launch{display:flex;flex-wrap:wrap;gap:12px;align-items:stretch;}' . '.sp-onboarding-launch-primary{display:inline-flex;align-items:center;justify-content:center;padding:10px 18px;border-radius:6px;background:#4F46E5;border:1px solid #4F46E5;color:#FFFFFF !important;font-size:13px;font-weight:600;line-height:1.4;text-decoration:none;cursor:pointer;}' . '.sp-onboarding-launch-primary:hover,.sp-onboarding-launch-primary:focus{background:#4338CA;border-color:#4338CA;color:#FFFFFF !important;}' . '.sp-onboarding-launch-trial{display:inline-flex;flex-direction:column;align-items:flex-start;justify-content:center;gap:2px;padding:10px 18px;border-radius:6px;border:1.5px solid #4F46E5;background:#FFFFFF;color:#4F46E5;text-decoration:none;min-width:160px;}' . '.sp-onboarding-launch-trial:hover,.sp-onboarding-launch-trial:focus{background:#EEF2FF;color:#4338CA;}' . '.sp-onboarding-launch-trial__label{font-size:13px;font-weight:600;line-height:1.3;}' . '.sp-onboarding-launch-trial__sub{font-size:12px;font-weight:400;line-height:1.3;color:#6B7280;}' );
        }
        if ( $hook_suffix === 'bobasoftware-publisher-paywall_page_bobasoftware-publisher-paywall-upgrade' || is_string( $hook_suffix ) && strpos( $hook_suffix, 'bobasoftware-publisher-paywall-upgrade' ) !== false ) {
            $upgrade_css = ( defined( 'BSPP_PLUGIN_URL' ) ? BSPP_PLUGIN_URL . 'admin/css/bspp-upgrade-page.css' : plugin_dir_url( dirname( __FILE__ ) ) . 'admin/css/bspp-upgrade-page.css' );
            wp_enqueue_style(
                'bspp-upgrade-page',
                $upgrade_css,
                array(),
                BSPP_VERSION
            );
        }
    }

    /**
     * Upgrade to Pro marketing page (free build).
     */
    public function render_upgrade_to_pro_page() {
        if ( !current_user_can( 'manage_options' ) ) {
            return;
        }
        // Don't show upgrade page to paying or trial users.
        if ( bspp_fs()->is_paying_or_trial() ) {
            echo '<div class="wrap bspp-upgrade-already">';
            echo '<h1>' . esc_html__( 'Publisher Paywall Pro', 'bobasoftware-publisher-paywall' ) . '</h1>';
            echo '<p>' . esc_html__( 'You already have access to Pro features. Thank you for being a Pro subscriber!', 'bobasoftware-publisher-paywall' ) . '</p>';
            echo '</div>';
            return;
        }
        $trial_url = esc_url( bspp_fs()->get_trial_url() );
        ?>
        <div class="bspp-upgrade-wrap">
            <div class="bspp-upgrade-hero">
                <h1><?php 
        esc_html_e( 'Bobasoftware Publisher Paywall Pro', 'bobasoftware-publisher-paywall' );
        ?></h1>
                <p class="bspp-upgrade-subheading"><?php 
        esc_html_e( 'Every way readers can pay you, in one plugin.', 'bobasoftware-publisher-paywall' );
        ?></p>
                <a href="<?php 
        echo esc_url( $trial_url );
        ?>" class="bspp-upgrade-btn-primary">
                    <?php 
        esc_html_e( 'Start free trial — 14 days, no credit card', 'bobasoftware-publisher-paywall' );
        ?>
                </a>
                <p class="bspp-upgrade-link-secondary">
                    <a href="https://bobasoftware.com/pricing" target="_blank" rel="noopener noreferrer">
                        <?php 
        esc_html_e( 'View pricing details', 'bobasoftware-publisher-paywall' );
        ?>
                    </a>
                </p>
            </div>

            <div class="bspp-upgrade-comparison">
                <h2><?php 
        esc_html_e( "What's included", 'bobasoftware-publisher-paywall' );
        ?></h2>
                <table>
                    <thead>
                        <tr>
                            <th><?php 
        esc_html_e( 'Feature', 'bobasoftware-publisher-paywall' );
        ?></th>
                            <th><?php 
        esc_html_e( 'Free', 'bobasoftware-publisher-paywall' );
        ?></th>
                            <th><?php 
        esc_html_e( 'Pro', 'bobasoftware-publisher-paywall' );
        ?></th>
                        </tr>
                    </thead>
                    <tbody>
                        <tr>
                            <td><?php 
        esc_html_e( 'Monthly subscription', 'bobasoftware-publisher-paywall' );
        ?></td>
                            <td>✓</td>
                            <td>✓</td>
                        </tr>
                        <tr>
                            <td><?php 
        esc_html_e( 'Annual subscription', 'bobasoftware-publisher-paywall' );
        ?></td>
                            <td>✓</td>
                            <td>✓</td>
                        </tr>
                        <tr>
                            <td><?php 
        esc_html_e( 'Free publish window', 'bobasoftware-publisher-paywall' );
        ?></td>
                            <td>✓</td>
                            <td>✓</td>
                        </tr>
                        <tr>
                            <td><?php 
        esc_html_e( 'Magic login for readers', 'bobasoftware-publisher-paywall' );
        ?></td>
                            <td>✓</td>
                            <td>✓</td>
                        </tr>
                        <tr>
                            <td><?php 
        esc_html_e( 'Members management', 'bobasoftware-publisher-paywall' );
        ?></td>
                            <td>✓</td>
                            <td>✓</td>
                        </tr>
                        <tr>
                            <td><?php 
        esc_html_e( 'Payments ledger', 'bobasoftware-publisher-paywall' );
        ?></td>
                            <td>✓</td>
                            <td>✓</td>
                        </tr>
                        <tr>
                            <td><?php 
        esc_html_e( 'Stripe integration', 'bobasoftware-publisher-paywall' );
        ?></td>
                            <td>✓</td>
                            <td>✓</td>
                        </tr>
                        <tr class="bspp-upgrade-pro-row">
                            <td><?php 
        esc_html_e( 'Tip unlock — readers pay what they want', 'bobasoftware-publisher-paywall' );
        ?></td>
                            <td>—</td>
                            <td>✓</td>
                        </tr>
                        <tr class="bspp-upgrade-pro-row">
                            <td><?php 
        esc_html_e( 'Per-article purchase — fixed price per post', 'bobasoftware-publisher-paywall' );
        ?></td>
                            <td>—</td>
                            <td>✓</td>
                        </tr>
                        <tr class="bspp-upgrade-pro-row">
                            <td><?php 
        esc_html_e( 'Free-post quota — give readers free posts', 'bobasoftware-publisher-paywall' );
        ?></td>
                            <td>—</td>
                            <td>✓</td>
                        </tr>
                        <tr class="bspp-upgrade-pro-row">
                            <td><?php 
        esc_html_e( 'Creator tools — preview & debug', 'bobasoftware-publisher-paywall' );
        ?></td>
                            <td>—</td>
                            <td>✓</td>
                        </tr>
                        <tr class="bspp-upgrade-pro-row">
                            <td><?php 
        esc_html_e( 'Priority email support', 'bobasoftware-publisher-paywall' );
        ?></td>
                            <td>—</td>
                            <td>✓</td>
                        </tr>
                    </tbody>
                </table>
            </div>

            <div class="bspp-upgrade-features">
                <h2><?php 
        esc_html_e( 'What Pro adds', 'bobasoftware-publisher-paywall' );
        ?></h2>
                <div class="bspp-upgrade-features-grid">
                    <div class="bspp-upgrade-feature-card">
                        <h3><?php 
        esc_html_e( 'Tip Unlock', 'bobasoftware-publisher-paywall' );
        ?></h3>
                        <p><?php 
        esc_html_e( 'Let readers choose their own price. Great for creators who don\'t want to lock content behind a fixed paywall. Set a suggested tip amount and a minimum. Readers who tip get instant access to that post.', 'bobasoftware-publisher-paywall' );
        ?></p>
                    </div>
                    <div class="bspp-upgrade-feature-card">
                        <h3><?php 
        esc_html_e( 'Per-Article Purchase', 'bobasoftware-publisher-paywall' );
        ?></h3>
                        <p><?php 
        esc_html_e( 'Sell individual posts at a fixed price. Readers who don\'t want a full subscription can pay to unlock just the piece they want.', 'bobasoftware-publisher-paywall' );
        ?></p>
                    </div>
                    <div class="bspp-upgrade-feature-card">
                        <h3><?php 
        esc_html_e( 'Free-Post Quota', 'bobasoftware-publisher-paywall' );
        ?></h3>
                        <p><?php 
        esc_html_e( 'Give logged-in readers a taste before asking for money. Choose between one-time (3 free posts total) or recurring (3 free posts per month). Perfect for building reader trust before conversion.', 'bobasoftware-publisher-paywall' );
        ?></p>
                    </div>
                    <div class="bspp-upgrade-feature-card">
                        <h3><?php 
        esc_html_e( 'Creator Tools', 'bobasoftware-publisher-paywall' );
        ?></h3>
                        <p><?php 
        esc_html_e( 'Preview how your paywall looks to different reader types. Debug why a specific post is locked or unlocked. See exactly what your readers see.', 'bobasoftware-publisher-paywall' );
        ?></p>
                    </div>
                </div>
            </div>

            <div class="bspp-upgrade-pricing">
                <h2><?php 
        esc_html_e( 'Pricing', 'bobasoftware-publisher-paywall' );
        ?></h2>
                <div class="bspp-upgrade-pricing-card">
                    <div>
                        <span class="bspp-upgrade-price"><?php 
        echo esc_html__( '$9.99', 'bobasoftware-publisher-paywall' );
        ?></span>
                        <span class="bspp-upgrade-price-period"><?php 
        esc_html_e( '/ month', 'bobasoftware-publisher-paywall' );
        ?></span>
                    </div>
                    <div class="bspp-upgrade-price-divider"><?php 
        esc_html_e( 'or', 'bobasoftware-publisher-paywall' );
        ?></div>
                    <div>
                        <span class="bspp-upgrade-price"><?php 
        echo esc_html__( '$99', 'bobasoftware-publisher-paywall' );
        ?></span>
                        <span class="bspp-upgrade-price-period"><?php 
        esc_html_e( '/ year — save $20 (2 months free)', 'bobasoftware-publisher-paywall' );
        ?></span>
                    </div>
                    <p class="bspp-upgrade-trial-note"><?php 
        esc_html_e( '14-day free trial. No credit card required.', 'bobasoftware-publisher-paywall' );
        ?></p>
                    <p class="bspp-upgrade-reassurance"><?php 
        esc_html_e( 'Cancel anytime. Your free-tier settings and data transfer automatically.', 'bobasoftware-publisher-paywall' );
        ?></p>
                    <div class="bspp-upgrade-pricing-cta">
                        <a href="<?php 
        echo esc_url( $trial_url );
        ?>" class="bspp-upgrade-btn-primary">
                            <?php 
        esc_html_e( 'Start free trial', 'bobasoftware-publisher-paywall' );
        ?>
                        </a>
                    </div>
                </div>
            </div>

            <div class="bspp-upgrade-faq">
                <h2><?php 
        esc_html_e( 'Common questions', 'bobasoftware-publisher-paywall' );
        ?></h2>

                <div class="bspp-upgrade-faq-item">
                    <h3><?php 
        esc_html_e( 'Will my settings and data transfer?', 'bobasoftware-publisher-paywall' );
        ?></h3>
                    <p><?php 
        esc_html_e( 'Yes. Pro is an upgrade of the same plugin — your subscription settings, Stripe keys, member data, and payment history all persist.', 'bobasoftware-publisher-paywall' );
        ?></p>
                </div>

                <div class="bspp-upgrade-faq-item">
                    <h3><?php 
        esc_html_e( 'What happens after the trial?', 'bobasoftware-publisher-paywall' );
        ?></h3>
                    <p><?php 
        esc_html_e( 'If you don\'t upgrade, your site automatically reverts to the free version with all your subscription features intact. Nothing breaks.', 'bobasoftware-publisher-paywall' );
        ?></p>
                </div>

                <div class="bspp-upgrade-faq-item">
                    <h3><?php 
        esc_html_e( 'Can I cancel anytime?', 'bobasoftware-publisher-paywall' );
        ?></h3>
                    <p><?php 
        esc_html_e( 'Yes. Cancel from the Freemius customer portal — your Pro features stay active until the end of your billing period.', 'bobasoftware-publisher-paywall' );
        ?></p>
                </div>

                <div class="bspp-upgrade-faq-item">
                    <h3><?php 
        esc_html_e( 'How do I get support?', 'bobasoftware-publisher-paywall' );
        ?></h3>
                    <p><?php 
        echo wp_kses_post( __( 'All users can email <a href="mailto:hi@bobasoftware.com">hi@bobasoftware.com</a>. Pro subscribers get priority response times.', 'bobasoftware-publisher-paywall' ) );
        ?></p>
                </div>
            </div>

            <div class="bspp-upgrade-bottom-cta">
                <h2><?php 
        esc_html_e( 'Ready to unlock more monetization?', 'bobasoftware-publisher-paywall' );
        ?></h2>
                <a href="<?php 
        echo esc_url( $trial_url );
        ?>" class="bspp-upgrade-btn-primary">
                    <?php 
        esc_html_e( 'Start free trial — 14 days, no credit card', 'bobasoftware-publisher-paywall' );
        ?>
                </a>
            </div>
        </div>
        <?php 
    }

    /**
     * Whether the guided onboarding has been completed (Launch paywall).
     */
    private function is_bspp_onboarding_completed() {
        $settings = new BSPP_Settings_Repository();
        return (int) $settings->get( 'bspp_onboarding_completed', 0 ) === 1;
    }

    /**
     * Whether Freemius is still in activation mode (opt-in neither completed nor skipped).
     *
     * While true, the Freemius SDK removes this plugin's submenu pages, so the Setup
     * wizard page is unreachable and must not be linked to or redirected to.
     *
     * Fails open: if Freemius is unavailable, behave as if activation is complete.
     *
     * @return bool
     */
    private function is_fs_activation_mode() {
        if ( !function_exists( 'bspp_fs' ) ) {
            return false;
        }
        $fs = bspp_fs();
        if ( !is_object( $fs ) || !method_exists( $fs, 'is_activation_mode' ) ) {
            return false;
        }
        return (bool) $fs->is_activation_mode();
    }

    /**
     * Block Bobasoftware Publisher Paywall product pages until onboarding is completed.
     */
    public function maybe_redirect_to_onboarding() {
        if ( !is_user_logged_in() || !current_user_can( 'manage_options' ) ) {
            return;
        }
        if ( wp_doing_ajax() ) {
            return;
        }
        // Freemius removes plugin submenus during activation mode, so redirecting to the
        // Setup wizard would land on "not allowed". Let the Freemius opt-in screen run first.
        if ( $this->is_fs_activation_mode() ) {
            return;
        }
        if ( !isset( $_GET['page'] ) || !is_string( $_GET['page'] ) ) {
            return;
        }
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- admin page routing, not a form submission.
        $page = sanitize_key( wp_unslash( $_GET['page'] ) );
        $gated = [
            'bobasoftware-publisher-paywall',
            'bobasoftware-publisher-paywall-members',
            'bobasoftware-publisher-paywall-payments',
            'bobasoftware-publisher-paywall-settings',
            'bobasoftware-publisher-paywall-tools'
        ];
        if ( !in_array( $page, $gated, true ) ) {
            return;
        }
        if ( $this->is_bspp_onboarding_completed() ) {
            return;
        }
        wp_safe_redirect( admin_url( 'admin.php?page=bobasoftware-publisher-paywall-onboarding' ) );
        exit;
    }

    /**
     * Mute Setup wizard submenu link globally in wp-admin (not page-gated).
     *
     * @param string $hook_suffix Current admin page hook suffix.
     */
    public function maybe_style_onboarding_submenu( $hook_suffix = '' ) {
        unset($hook_suffix);
        if ( !$this->is_bspp_onboarding_completed() ) {
            return;
        }
        wp_enqueue_style( 'wp-admin' );
        wp_add_inline_style( 'wp-admin', '#adminmenu .wp-submenu a[href*="page=bobasoftware-publisher-paywall-onboarding"]{opacity:.72;font-size:12.5px;}' );
    }

    public function maybe_apply_paywall( $content ) {
        if ( !is_singular( 'post' ) ) {
            return $content;
        }
        $post_id = get_the_ID();
        $user_id = get_current_user_id();
        $paywall = new BSPP_Paywall_Engine();
        $can_access = $paywall->is_post_unlocked( $post_id, $user_id );
        if ( $can_access ) {
            if ( class_exists( 'BSPP_Paywall_Overlay' ) ) {
                $banner = BSPP_Paywall_Overlay::render_unlocked_free_indicator_banner( $post_id );
                if ( $banner !== '' ) {
                    return $banner . $content;
                }
            }
            return $content;
        }
        return BSPP_Paywall_Overlay::render( $post_id, $content );
    }

    /**
     * Append a subtle manage-subscription note after unlocked singular content.
     *
     * @param string $content Post content HTML.
     * @return string
     */
    public function maybe_append_manage_subscription_after_content( $content ) {
        if ( is_admin() || is_feed() || function_exists( 'wp_is_json_request' ) && wp_is_json_request() ) {
            return $content;
        }
        if ( !is_singular( 'post' ) || !in_the_loop() || !is_main_query() ) {
            return $content;
        }
        $portal_url = self::get_manage_subscription_portal_url_for_user();
        if ( $portal_url === '' ) {
            return $content;
        }
        $post_id = get_the_ID();
        if ( $post_id <= 0 ) {
            return $content;
        }
        if ( !class_exists( 'BSPP_Paywall_Overlay' ) || !method_exists( 'BSPP_Paywall_Overlay', 'is_post_locked_for_visitor' ) ) {
            return $content;
        }
        if ( BSPP_Paywall_Overlay::is_post_locked_for_visitor( $post_id ) ) {
            return $content;
        }
        $note = '<div class="bspp-manage-subscription-note">' . '<p class="bspp-manage-subscription-note__line">' . '<span class="bspp-manage-subscription-note__label">' . esc_html__( 'You\'re a subscriber', 'bobasoftware-publisher-paywall' ) . ' · ' . '</span>' . '<a class="bspp-manage-subscription-note__link" href="' . esc_url( $portal_url ) . '">' . esc_html__( 'Manage subscription', 'bobasoftware-publisher-paywall' ) . '</a>' . '</p>' . '</div>';
        return $content . $note;
    }

    /**
     * Guidance tooltip (ⓘ) for Access & Monetization — not for validation errors.
     */
    private function render_access_tooltip( $id, $text ) {
        $id = sanitize_key( (string) $id );
        if ( $id === '' ) {
            $id = 'sp-tip';
        }
        $html = '<span class="sp-tip-wrap">' . '<button type="button" class="sp-tip-btn" aria-label="More information" aria-describedby="' . esc_attr( $id ) . '">' . '<span class="sp-tip-icon" aria-hidden="true">ⓘ</span></button>' . '<span id="' . esc_attr( $id ) . '" class="sp-tip-bubble" role="tooltip">' . esc_html( $text ) . '</span></span>';
        return wp_kses( $html, [
            'span'   => [
                'class'       => true,
                'id'          => true,
                'role'        => true,
                'aria-hidden' => true,
            ],
            'button' => [
                'type'             => true,
                'class'            => true,
                'aria-label'       => true,
                'aria-describedby' => true,
            ],
        ] );
    }

    public function render_access_monetization_page() {
        $settings = new BSPP_Settings_Repository();
        $d = BSPP_Settings_Repository::defaults();
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- not a form submission, display-only URL parameter.
        $status = ( isset( $_GET['bspp_access_monetization'] ) ? sanitize_key( wp_unslash( $_GET['bspp_access_monetization'] ) ) : '' );
        $saved = $status === 'saved';
        $tip_error = $status === 'tip_error';
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- not a form submission, display-only URL parameter.
        $onboarding_launch_done = isset( $_GET['bspp_onboarding_launch_done'] ) && sanitize_key( wp_unslash( (string) $_GET['bspp_onboarding_launch_done'] ) ) === '1';
        $enable_subscription = (int) $settings->get( 'enable_subscription', $d['enable_subscription'] );
        $subscription_price_monthly = (float) $settings->get( 'subscription_price_monthly', $d['subscription_price_monthly'] );
        $subscription_price_annual = (float) $settings->get( 'subscription_price_annual', $d['subscription_price_annual'] );
        $posts_become_paid_after_days = (int) $settings->get( 'posts_become_paid_after_days', $d['posts_become_paid_after_days'] );
        // Pro settings only exist in Pro package (defaults + UI wrapped similarly).
        $enable_tip_unlock = 0;
        $default_tip_amount = 0.0;
        $minimum_tip_amount = 0.0;
        $enable_article_unlock = 0;
        $article_unlock_price = 0.0;
        $free_access_model = 'one_time';
        $free_article_limit = 0;
        $bspp_premium = false;
        if ( $tip_error && $bspp_premium ) {
            $form_state = get_transient( 'bobasoftware_pp_access_monetization_form_' . get_current_user_id() );
            if ( is_array( $form_state ) ) {
                $enable_subscription = ( isset( $form_state['enable_subscription'] ) ? (int) $form_state['enable_subscription'] : $enable_subscription );
                $subscription_price_monthly = ( isset( $form_state['subscription_price_monthly'] ) ? (float) $form_state['subscription_price_monthly'] : $subscription_price_monthly );
                $subscription_price_annual = ( isset( $form_state['subscription_price_annual'] ) ? (float) $form_state['subscription_price_annual'] : $subscription_price_annual );
                $enable_tip_unlock = ( isset( $form_state['enable_tip_unlock'] ) ? (int) $form_state['enable_tip_unlock'] : $enable_tip_unlock );
                $default_tip_amount = ( isset( $form_state['default_tip_amount'] ) ? (float) $form_state['default_tip_amount'] : $default_tip_amount );
                $minimum_tip_amount = ( isset( $form_state['minimum_tip_amount'] ) ? (float) $form_state['minimum_tip_amount'] : $minimum_tip_amount );
                $enable_article_unlock = ( isset( $form_state['enable_article_unlock'] ) ? (int) $form_state['enable_article_unlock'] : $enable_article_unlock );
                $article_unlock_price = ( isset( $form_state['article_unlock_price'] ) ? (float) $form_state['article_unlock_price'] : $article_unlock_price );
                $free_access_model = ( isset( $form_state['free_access_model'] ) ? sanitize_key( (string) $form_state['free_access_model'] ) : $free_access_model );
                $free_article_limit = ( isset( $form_state['free_article_limit'] ) ? (int) $form_state['free_article_limit'] : $free_article_limit );
                $posts_become_paid_after_days = ( isset( $form_state['posts_become_paid_after_days'] ) ? (int) $form_state['posts_become_paid_after_days'] : $posts_become_paid_after_days );
            }
        }
        // legacy tip_error block removed below
        $sub_mo_disp = number_format(
            $subscription_price_monthly,
            2,
            '.',
            ''
        );
        $sub_yr_disp = number_format(
            $subscription_price_annual,
            2,
            '.',
            ''
        );
        $tip_def_disp = number_format(
            max( 0, (float) $default_tip_amount ),
            2,
            '.',
            ''
        );
        $tip_min_disp = number_format(
            max( 0, (float) $minimum_tip_amount ),
            2,
            '.',
            ''
        );
        $art_price_disp = number_format(
            $article_unlock_price,
            2,
            '.',
            ''
        );
        echo '<div class="wrap sp-am-wrap">';
        echo '<h1>Access & Monetization</h1>';
        echo '<p class="sp-am-lede">Configure how readers access and pay for your posts.</p>';
        $this->bspp_maybe_render_email_status_banner();
        if ( $saved ) {
            echo '<div class="notice notice-success is-dismissible"><p>Access & Monetization settings saved.</p></div>';
        }
        if ( $onboarding_launch_done ) {
            echo '<div class="notice notice-success is-dismissible"><p>Your paywall is live. Readers will see premium content on locked posts. Adjust options below anytime.</p></div>';
        }
        if ( $tip_error ) {
            echo '<div class="notice notice-error"><p>Please fix validation errors before saving.</p></div>';
        }
        echo '<form id="sp-access-monetization-form" method="post" action="' . esc_url( admin_url( 'admin-post.php' ) ) . '">';
        echo '<input type="hidden" name="action" value="bspp_save_access_monetization" />';
        wp_nonce_field( 'bspp_save_access_monetization', 'bspp_access_monetization_nonce' );
        echo '<div class="sp-am-stack">';
        echo '<div class="sp-am-section">';
        $monetization_tooltip = 'Choose how readers can access and pay for your posts.';
        $monetization_desc = __( 'Choose how readers can access and pay for your posts.', 'bobasoftware-publisher-paywall' );
        echo '<h2 class="sp-am-section__title">💰 Monetization' . wp_kses_post( $this->render_access_tooltip( 'sp-tip-monetization-section', $monetization_tooltip ) ) . '</h2>';
        echo '<p class="sp-am-section__desc">' . esc_html( $monetization_desc ) . '</p>';
        echo '</div>';
        echo '<div class="sp-am-card sp-am-card--subscription' . (( $enable_subscription ? '' : ' sp-am-card--off' )) . '">';
        echo '<div class="sp-am-card__head">';
        echo '<div><h3 class="sp-am-card__title">💳 Subscription</h3><p class="sp-am-card__desc">Charge readers a recurring fee for full access to your posts.</p></div>';
        echo '<label class="sp-switch"><input type="checkbox" class="sp-switch-input" id="enable_subscription" name="enable_subscription" value="1" role="switch" ' . checked( $enable_subscription, 1, false ) . ' /><span class="sp-switch-ui" aria-hidden="true"></span></label>';
        echo '</div>';
        echo '<div class="sp-am-card__body"><div class="sp-am-grid-2">';
        echo '<div class="sp-am-field"><span class="sp-am-field__label" id="sp-label-subscription-monthly"><span class="sp-label-text">Monthly price (USD)</span>' . wp_kses_post( $this->render_access_tooltip( 'sp-tip-subscription-monthly', 'A common starting point is $9.99/month.' ) ) . '</span><input class="sp-am-field__input" type="number" id="subscription_price_monthly" name="subscription_price_monthly" min="0" step="0.01" value="' . esc_attr( $sub_mo_disp ) . '" aria-labelledby="sp-label-subscription-monthly" ' . disabled( $enable_subscription, 0, false ) . ' /></div>';
        echo '<div class="sp-am-field"><span class="sp-am-field__label" id="sp-label-subscription-annual"><span class="sp-label-text">Annual price (USD)</span>' . wp_kses_post( $this->render_access_tooltip( 'sp-tip-subscription-annual', 'Usually priced a bit lower than monthly (about 20% off).' ) ) . '</span><input class="sp-am-field__input" type="number" id="subscription_price_annual" name="subscription_price_annual" min="0" step="0.01" value="' . esc_attr( $sub_yr_disp ) . '" aria-labelledby="sp-label-subscription-annual" ' . disabled( $enable_subscription, 0, false ) . ' /></div>';
        echo '</div></div></div>';
        echo '<div class="sp-am-card sp-am-card--publish-window">';
        echo '<div class="sp-am-card__head">';
        echo '<div><h3 class="sp-am-card__title">' . esc_html__( 'Free Publish Window', 'bobasoftware-publisher-paywall' ) . '</h3><p class="sp-am-card__desc">' . esc_html__( 'New posts stay free for this many days before the paywall activates.', 'bobasoftware-publisher-paywall' ) . '</p></div>';
        echo '</div>';
        echo '<div class="sp-am-card__body">';
        echo '<div class="sp-am-field"><span class="sp-am-field__label" id="sp-label-posts-days"><span class="sp-label-text">Posts become paid after X days</span>' . wp_kses_post( $this->render_access_tooltip( 'sp-tip-posts-paid-days', '7 days is a good starting point.' ) ) . '</span><input class="sp-am-field__input" type="number" id="posts_become_paid_after_days" name="posts_become_paid_after_days" min="0" step="1" value="' . esc_attr( $posts_become_paid_after_days ) . '" aria-labelledby="sp-label-posts-days" /></div>';
        echo '</div></div>';
        echo '</div>';
        $recommended_defaults = wp_json_encode( BSPP_Settings_Repository::defaults(), JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT );
        echo '<p class="sp-am-form-actions"><button type="button" id="sp-reset-access-monetization-defaults" class="button">Reset to recommended defaults</button> ' . '<button id="sp-save-access-monetization" type="submit" class="button button-primary">Save settings</button></p>';
        echo '</form>';
        // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- JSON from wp_json_encode with hex flags.
        wp_add_inline_script( 'bspp-admin-scripts', 'var spAccessMonetizationDefaults = ' . $recommended_defaults . ';', 'before' );
        wp_add_inline_script( 'bspp-admin-scripts', 'var spIsPremium = ' . (( $bspp_premium ? 'true' : 'false' )) . ';', 'before' );
        $trial_url = esc_url( bspp_fs()->get_trial_url() );
        $learn_more_url = esc_url( admin_url( 'admin.php?page=bobasoftware-publisher-paywall-upgrade' ) );
        echo '<section class="sp-am-pro-upsell" aria-labelledby="sp-am-pro-upsell-heading">';
        echo '<h2 id="sp-am-pro-upsell-heading" class="sp-am-pro-upsell__title">' . esc_html__( 'Unlock more with Pro', 'bobasoftware-publisher-paywall' ) . '</h2>';
        echo '<div class="sp-am-pro-upsell__cards">';
        echo '<article class="sp-am-pro-upsell__card">';
        echo '<h3 class="sp-am-pro-upsell__card-title">' . esc_html__( 'Tip Unlock', 'bobasoftware-publisher-paywall' ) . '</h3>';
        echo '<p class="sp-am-pro-upsell__card-body">' . esc_html__( 'Let readers choose their own price to unlock a post. Set a suggested and minimum tip amount.', 'bobasoftware-publisher-paywall' ) . '</p>';
        echo '</article>';
        echo '<article class="sp-am-pro-upsell__card">';
        echo '<h3 class="sp-am-pro-upsell__card-title">' . esc_html__( 'Per-Article Purchase', 'bobasoftware-publisher-paywall' ) . '</h3>';
        echo '<p class="sp-am-pro-upsell__card-body">' . esc_html__( 'Sell individual posts at a fixed price. Perfect for readers who don\'t want a full subscription.', 'bobasoftware-publisher-paywall' ) . '</p>';
        echo '</article>';
        echo '<article class="sp-am-pro-upsell__card">';
        echo '<h3 class="sp-am-pro-upsell__card-title">' . esc_html__( 'Free-Post Quota', 'bobasoftware-publisher-paywall' ) . '</h3>';
        echo '<p class="sp-am-pro-upsell__card-body">' . esc_html__( 'Give logged-in readers a taste — 3 free posts total or 3 free posts per month.', 'bobasoftware-publisher-paywall' ) . '</p>';
        echo '</article>';
        echo '</div>';
        echo '<div class="sp-am-pro-upsell__actions">';
        echo '<a class="sp-am-pro-upsell__cta" href="' . esc_url( $trial_url ) . '">' . esc_html__( 'Start free trial — 14 days, no credit card', 'bobasoftware-publisher-paywall' ) . '</a>';
        echo '<a class="sp-am-pro-upsell__learn" href="' . esc_url( $learn_more_url ) . '">' . esc_html__( 'Learn more about Pro →', 'bobasoftware-publisher-paywall' ) . '</a>';
        echo '</div>';
        echo '</section>';
        echo '</div>';
    }

    /**
     * Build a masked display string for a stored secret: keep the prefix and last 4
     * characters, replace the middle with a fixed run of bullets (length not revealed).
     * Returns '' for an empty value. Display-only — never used for storage.
     */
    private function bspp_mask_secret( $value ) {
        $value = (string) $value;
        $len = strlen( $value );
        if ( $len === 0 ) {
            return '';
        }
        $bullets = str_repeat( '•', 12 );
        if ( $len <= 12 ) {
            return $bullets;
        }
        return substr( $value, 0, 8 ) . $bullets . substr( $value, -4 );
    }

    public function render_settings_page() {
        $settings = new BSPP_Settings_Repository();
        $stripe_api_key = $settings->get( 'stripe_api_key', '' );
        $stripe_webhook_secret = $settings->get( 'stripe_webhook_secret', '' );
        $stripe_connected = trim( (string) $stripe_api_key ) !== '' && trim( (string) $stripe_webhook_secret ) !== '';
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- not a form submission, display-only URL parameter.
        $saved = isset( $_GET['bspp_settings'] ) && sanitize_key( wp_unslash( (string) $_GET['bspp_settings'] ) ) === 'saved';
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- not a form submission, display-only URL parameter.
        $email_test_result = ( isset( $_GET['bspp_email_test'] ) ? sanitize_key( wp_unslash( (string) $_GET['bspp_email_test'] ) ) : '' );
        $email_status = $this->email_health->bspp_get_email_status();
        $email_last_error = $this->email_health->bspp_get_last_error();
        $webhook_url = admin_url( 'admin-post.php?action=bspp_stripe_webhook' );
        echo '<div class="wrap sp-stripe-settings-wrap">';
        echo '<h1>Publisher Paywall Settings</h1>';
        $this->bspp_maybe_render_email_status_banner();
        if ( $saved ) {
            echo '<div class="notice notice-success is-dismissible"><p>Settings saved.</p></div>';
        }
        if ( $email_test_result === 'success' ) {
            echo '<div class="notice notice-success is-dismissible"><p>' . esc_html__( 'Test email sent. Check your inbox to confirm delivery.', 'bobasoftware-publisher-paywall' ) . '</p></div>';
        } elseif ( $email_test_result === 'failed' ) {
            $fail_message = ( $email_last_error !== null && $email_last_error !== '' ? $email_last_error : __( 'The test email could not be sent.', 'bobasoftware-publisher-paywall' ) );
            echo '<div class="notice notice-error is-dismissible"><p>' . esc_html( $fail_message ) . '</p></div>';
        }
        echo '<div class="sp-stripe-setup-card">';
        $status_class = ( $stripe_connected ? 'sp-stripe-connect-status is-connected' : 'sp-stripe-connect-status' );
        echo '<div class="sp-stripe-setup-card__header">';
        echo '<h2 class="sp-stripe-setup-card__title">💳 Connect Stripe</h2>';
        echo '<div id="sp-stripe-connect-status" class="' . esc_attr( $status_class ) . '" role="status" aria-live="polite"' . ' data-label-connected="' . esc_attr( __( 'Connected', 'bobasoftware-publisher-paywall' ) ) . '"' . ' data-label-not="' . esc_attr( __( 'Not connected', 'bobasoftware-publisher-paywall' ) ) . '">';
        echo '<div class="sp-stripe-connect-status__row"><span class="sp-stripe-connect-status__dot" aria-hidden="true"></span>';
        echo '<span class="sp-stripe-connect-status__label">' . (( $stripe_connected ? esc_html__( 'Connected', 'bobasoftware-publisher-paywall' ) : esc_html__( 'Not connected', 'bobasoftware-publisher-paywall' ) )) . '</span></div>';
        echo '<span class="sp-stripe-connect-status__sub">' . esc_html__( 'Payments are ready', 'bobasoftware-publisher-paywall' ) . '</span>';
        echo '</div></div>';
        echo '<p class="sp-stripe-setup-card__lede">Takes about 2 minutes.</p>';
        echo '<form id="sp-settings-form" method="post" action="' . esc_url( admin_url( 'admin-post.php' ) ) . '">';
        echo '<input type="hidden" name="action" value="bspp_save_stripe_settings" />';
        wp_nonce_field( 'bspp_save_stripe_settings', 'bspp_stripe_settings_nonce' );
        echo '<div class="sp-stripe-step">';
        echo '<div class="sp-stripe-step__marker" aria-hidden="true">1</div>';
        echo '<div class="sp-stripe-step__content">';
        echo '<p class="sp-stripe-step__title">Copy webhook URL</p>';
        echo '<div class="sp-stripe-step__field">';
        echo '<label for="sp-stripe-webhook-url">Webhook URL</label>';
        echo '<div class="sp-stripe-webhook-row">';
        echo '<input type="text" id="sp-stripe-webhook-url" class="regular-text" value="' . esc_attr( $webhook_url ) . '" readonly autocomplete="off" />';
        echo '<button type="button" class="button" id="sp-copy-webhook-url" aria-label="Copy webhook URL to clipboard">Copy</button>';
        echo '</div></div>';
        echo '<p class="sp-stripe-step__helper">Copy this URL. You will use it in Stripe.</p>';
        echo '</div></div>';
        echo '<div class="sp-stripe-step sp-stripe-step--textonly">';
        echo '<div class="sp-stripe-step__marker" aria-hidden="true">2</div>';
        echo '<div class="sp-stripe-step__content">';
        echo '<p class="sp-stripe-step__title">Create webhook in Stripe</p>';
        echo '<p class="sp-stripe-step__helper">In Stripe, go to Developers → Webhooks → Add destination. Select these three events, then paste the URL from Step 1 into the Endpoint URL field.</p>';
        echo '<ul class="sp-stripe-step__helper">';
        echo '<li>checkout.session.completed</li>';
        echo '<li>customer.subscription.updated</li>';
        echo '<li>customer.subscription.deleted</li>';
        echo '</ul>';
        echo '</div></div>';
        echo '<div class="sp-stripe-step">';
        echo '<div class="sp-stripe-step__marker" aria-hidden="true">3</div>';
        echo '<div class="sp-stripe-step__content">';
        echo '<p class="sp-stripe-step__title">Paste webhook secret</p>';
        echo '<div class="sp-stripe-step__field">';
        echo '<label for="stripe_webhook_secret">Stripe webhook secret</label>';
        $wh_has_value = trim( (string) $stripe_webhook_secret ) !== '';
        echo '<div class="sp-secret-field">';
        echo '<input type="text" class="regular-text sp-secret-field__masked" value="' . esc_attr( $this->bspp_mask_secret( $stripe_webhook_secret ) ) . '" readonly tabindex="-1" aria-hidden="true"' . (( $wh_has_value ? '' : ' style="display:none;"' )) . ' />';
        echo '<input type="text" id="stripe_webhook_secret" name="stripe_webhook_secret" value="' . esc_attr( $stripe_webhook_secret ) . '" class="regular-text sp-secret-field__real" autocomplete="off"' . (( $wh_has_value ? ' style="display:none;"' : '' )) . ' />';
        echo '<button type="button" class="button-link sp-secret-field__toggle" aria-pressed="false"' . (( $wh_has_value ? '' : ' style="display:none;"' )) . '>Reveal</button>';
        echo '</div>';
        echo '</div>';
        echo '<p class="sp-stripe-step__helper">Paste the signing secret from Stripe (starts with <code>whsec_</code>…).</p>';
        echo '</div></div>';
        echo '<div class="sp-stripe-step">';
        echo '<div class="sp-stripe-step__marker" aria-hidden="true">4</div>';
        echo '<div class="sp-stripe-step__content">';
        echo '<p class="sp-stripe-step__title">Add Stripe API key</p>';
        echo '<div class="sp-stripe-step__field">';
        echo '<label for="stripe_api_key">Stripe API key (secret)</label>';
        $key_has_value = trim( (string) $stripe_api_key ) !== '';
        echo '<div class="sp-secret-field">';
        echo '<input type="text" class="regular-text sp-secret-field__masked" value="' . esc_attr( $this->bspp_mask_secret( $stripe_api_key ) ) . '" readonly tabindex="-1" aria-hidden="true"' . (( $key_has_value ? '' : ' style="display:none;"' )) . ' />';
        echo '<input type="text" id="stripe_api_key" name="stripe_api_key" value="' . esc_attr( $stripe_api_key ) . '" class="regular-text sp-secret-field__real" autocomplete="off" spellcheck="false"' . (( $key_has_value ? ' style="display:none;"' : '' )) . ' />';
        echo '<button type="button" class="button-link sp-secret-field__toggle" aria-pressed="false"' . (( $key_has_value ? '' : ' style="display:none;"' )) . '>Reveal</button>';
        echo '</div>';
        echo '</div>';
        echo '<p class="sp-stripe-step__helper">Find this in Stripe → Developers → API keys. Paste your secret key (starts with <code>sk_</code>…).</p>';
        echo '</div></div>';
        echo '<div class="sp-stripe-setup-card__actions"><button id="sp-save-settings" type="submit" class="button button-primary">Save</button></div>';
        echo '</form>';
        echo '</div>';
        $email_status_class = 'sp-email-delivery-status';
        $email_status_label = __( 'Not configured', 'bobasoftware-publisher-paywall' );
        if ( $email_status === BSPP_Email_Health::STATUS_WORKING ) {
            $email_status_class .= ' is-working';
            $email_status_label = __( 'Working', 'bobasoftware-publisher-paywall' );
        } elseif ( $email_status === BSPP_Email_Health::STATUS_FAILING ) {
            $email_status_class .= ' is-failing';
            $email_status_label = __( 'Failing', 'bobasoftware-publisher-paywall' );
        } else {
            $email_status_class .= ' is-not-configured';
        }
        echo '<div class="sp-stripe-setup-card">';
        echo '<div class="sp-stripe-setup-card__header">';
        echo '<h2 class="sp-stripe-setup-card__title">📧 ' . esc_html__( 'Email Delivery', 'bobasoftware-publisher-paywall' ) . '</h2>';
        echo '<div id="sp-email-delivery-status" class="' . esc_attr( $email_status_class ) . '" role="status" aria-live="polite">';
        echo '<div class="sp-email-delivery-status__row"><span class="sp-email-delivery-status__dot" aria-hidden="true"></span>';
        echo '<span class="sp-email-delivery-status__label">' . esc_html( $email_status_label ) . '</span></div>';
        echo '</div></div>';
        echo '<p class="sp-stripe-setup-card__lede">' . esc_html__( 'Magic login links depend on your site being able to send email.', 'bobasoftware-publisher-paywall' ) . '</p>';
        if ( $email_status === BSPP_Email_Health::STATUS_FAILING && $email_last_error !== null && $email_last_error !== '' ) {
            echo '<p class="sp-email-delivery-status__error">' . esc_html( $email_last_error ) . '</p>';
        }
        echo '<form method="post" action="' . esc_url( admin_url( 'admin-post.php' ) ) . '">';
        echo '<input type="hidden" name="action" value="bspp_send_test_email" />';
        wp_nonce_field( 'bspp_send_test_email', 'bspp_email_test_nonce' );
        echo '<div class="sp-stripe-setup-card__actions">';
        echo '<button type="submit" class="button">' . esc_html__( 'Send test email', 'bobasoftware-publisher-paywall' ) . '</button>';
        echo '</div></form>';
        echo '</div>';
        echo '</div>';
    }

    /**
     * Best URL for the publisher to set up outbound email (SMTP plugin or install).
     */
    private function bspp_get_email_setup_url() {
        if ( !function_exists( 'is_plugin_active' ) ) {
            require_once ABSPATH . 'wp-admin/includes/plugin.php';
        }
        if ( is_plugin_active( 'wp-mail-smtp/wp_mail_smtp.php' ) ) {
            return admin_url( 'admin.php?page=wp-mail-smtp' );
        }
        if ( is_plugin_active( 'easy-wp-smtp/easy-wp-smtp.php' ) ) {
            return admin_url( 'admin.php?page=swpsmtp_settings' );
        }
        return admin_url( 'plugin-install.php?s=wp-mail-smtp&tab=search&type=term' );
    }

    /**
     * Persistent status banner for unresolved email setup (design-guidelines.md).
     * Call only from Access & Monetization and Settings admin pages.
     */
    private function bspp_maybe_render_email_status_banner() {
        $status = $this->email_health->bspp_get_email_status();
        if ( $status === BSPP_Email_Health::STATUS_WORKING ) {
            return;
        }
        if ( $status === BSPP_Email_Health::STATUS_FAILING ) {
            $modifier = 'is-error';
        } elseif ( $status === BSPP_Email_Health::STATUS_NOT_CONFIGURED ) {
            $modifier = 'is-warning';
        } else {
            return;
        }
        echo '<div class="sp-persistent-status-banner ' . esc_attr( $modifier ) . '" role="status">';
        echo '<span class="sp-persistent-status-banner__text">';
        echo esc_html__( '⚠️ Readers may not be able to log back in — your site\'s email isn\'t set up yet.', 'bobasoftware-publisher-paywall' );
        echo ' ';
        echo '<a class="sp-persistent-status-banner__link" href="' . esc_url( $this->bspp_get_email_setup_url() ) . '">' . esc_html__( 'Fix this', 'bobasoftware-publisher-paywall' ) . '</a>';
        echo '</span></div>';
    }

    /**
     * Reusable second step for "continue despite a warning" (design-guidelines.md).
     *
     * @param string $panel_id          Base DOM id (initial panel uses {$panel_id}-initial).
     * @param string $consequence_text  Restated consequence shown before Go back / Continue anyway.
     */
    private function bspp_render_continue_despite_warning_confirm( $panel_id, $consequence_text ) {
        echo '<div id="' . esc_attr( $panel_id ) . '-confirm" class="sp-continue-despite-warning__confirm sp-onboarding-panel-hidden" role="region" aria-labelledby="' . esc_attr( $panel_id ) . '-confirm-title">';
        echo '<p id="' . esc_attr( $panel_id ) . '-confirm-title" class="sp-continue-despite-warning__consequence">' . esc_html( $consequence_text ) . '</p>';
        echo '<div class="sp-continue-despite-warning__actions">';
        echo '<button type="button" class="button sp-continue-despite-warning__go-back" data-panel-id="' . esc_attr( $panel_id ) . '">' . esc_html__( 'Go back', 'bobasoftware-publisher-paywall' ) . '</button>';
        echo '<button type="button" class="button sp-continue-despite-warning__continue" data-panel-id="' . esc_attr( $panel_id ) . '">' . esc_html__( 'Continue anyway', 'bobasoftware-publisher-paywall' ) . '</button>';
        echo '</div></div>';
    }

    /**
     * Email health UI for onboarding step 3 (alongside Connect Stripe).
     *
     * @param string $email_test_result Sanitized bspp_email_test query value (success|failed|'').
     */
    private function bspp_render_onboarding_email_section( $email_test_result ) {
        $email_status = $this->email_health->bspp_get_email_status();
        $email_last_error = $this->email_health->bspp_get_last_error();
        $email_working = $email_status === BSPP_Email_Health::STATUS_WORKING;
        echo '<div class="sp-onboarding-email-section sp-onboarding-subblock" id="sp-email-onboarding">';
        if ( $email_test_result === 'success' ) {
            echo '<div class="notice notice-success inline sp-onboarding-email-notice"><p>' . esc_html__( 'Test email sent. Check your inbox to confirm delivery.', 'bobasoftware-publisher-paywall' ) . '</p></div>';
        } elseif ( $email_test_result === 'failed' ) {
            $fail_message = ( $email_last_error !== null && $email_last_error !== '' ? $email_last_error : __( 'The test email could not be sent.', 'bobasoftware-publisher-paywall' ) );
            echo '<div class="notice notice-error inline sp-onboarding-email-notice"><p>' . esc_html( $fail_message ) . '</p></div>';
        }
        if ( $email_working ) {
            echo '<div class="sp-onboarding-email-pass" role="status">';
            echo '<span class="sp-onboarding-email-pass__dot" aria-hidden="true"></span>';
            echo '<span class="sp-onboarding-email-pass__label">' . esc_html__( 'Email delivery is working', 'bobasoftware-publisher-paywall' ) . '</span>';
            echo '</div>';
        } else {
            echo '<div id="sp-email-onboarding-initial" class="sp-email-setup-ack-panel">';
            echo '<h3 class="sp-email-setup-ack-panel__heading">';
            echo '<span class="sp-email-setup-ack-panel__icon" aria-hidden="true">⚠</span>';
            echo esc_html__( "We couldn't confirm your site can send email", 'bobasoftware-publisher-paywall' );
            echo '</h3>';
            echo '<p class="sp-email-setup-ack-panel__body">';
            echo esc_html__( 'Magic login links are how readers get back into their account after paying. Without working email, returning readers may not be able to log back in.', 'bobasoftware-publisher-paywall' );
            echo '</p>';
            if ( $email_status === BSPP_Email_Health::STATUS_FAILING && $email_last_error !== null && $email_last_error !== '' ) {
                echo '<p class="sp-email-setup-ack-panel__error">' . esc_html( $email_last_error ) . '</p>';
            }
            echo '<div class="sp-email-setup-ack-panel__actions">';
            echo '<a href="' . esc_url( $this->bspp_get_email_setup_url() ) . '" class="button button-primary">' . esc_html__( 'Set up email now', 'bobasoftware-publisher-paywall' ) . '</a>';
            echo '<form class="sp-email-setup-ack-panel__test-form" method="post" action="' . esc_url( admin_url( 'admin-post.php' ) ) . '">';
            echo '<input type="hidden" name="action" value="bspp_send_test_email" />';
            echo '<input type="hidden" name="bspp_email_return_to" value="onboarding" />';
            wp_nonce_field( 'bspp_send_test_email', 'bspp_email_test_nonce' );
            echo '<button type="submit" class="button">' . esc_html__( 'Send test email', 'bobasoftware-publisher-paywall' ) . '</button>';
            echo '</form>';
            echo '</div>';
            echo '<p class="sp-email-setup-ack-panel__secondary">';
            echo '<a href="#" class="sp-continue-despite-warning__trigger" data-panel-id="sp-email-onboarding">' . esc_html__( 'Continue without email setup', 'bobasoftware-publisher-paywall' ) . '</a>';
            echo '</p>';
            echo '</div>';
            $this->bspp_render_continue_despite_warning_confirm( 'sp-email-onboarding', __( 'Are you sure? Readers won\'t be able to log back in to their accounts until this is set up.', 'bobasoftware-publisher-paywall' ) );
        }
        echo '</div>';
    }

    /**
     * Show onboarding entry prompt until the creator launches the paywall.
     */
    public function maybe_show_onboarding_notice() {
        if ( !current_user_can( 'manage_options' ) ) {
            return;
        }
        // Don't advertise the Setup wizard while Freemius has it removed.
        if ( $this->is_fs_activation_mode() ) {
            return;
        }
        $settings = new BSPP_Settings_Repository();
        $completed = (int) $settings->get( 'bspp_onboarding_completed', 0 );
        if ( $completed === 1 ) {
            return;
        }
        // Avoid interrupting the onboarding flow itself.
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- not a form submission, admin page routing.
        $current_page = ( isset( $_GET['page'] ) ? sanitize_key( wp_unslash( (string) $_GET['page'] ) ) : '' );
        if ( $current_page === 'bobasoftware-publisher-paywall-onboarding' ) {
            return;
        }
        $url = admin_url( 'admin.php?page=bobasoftware-publisher-paywall-onboarding' );
        echo '<div class="notice notice-info is-dismissible"><p>' . '<strong>Let\'s set up your paywall</strong> — under 2 minutes. ' . '<a href="' . esc_url( $url ) . '">Open onboarding</a>' . '</p></div>';
    }

    public function render_onboarding_page() {
        if ( !current_user_can( 'manage_options' ) ) {
            return;
        }
        $settings = new BSPP_Settings_Repository();
        $d = BSPP_Settings_Repository::defaults();
        $completed = (int) $settings->get( 'bspp_onboarding_completed', 0 );
        $paywall_launched = (int) $settings->get( 'bspp_paywall_launched', 0 );
        $step1_done = (int) $settings->get( 'bspp_onboarding_step1_done', 0 );
        $step2_done = (int) $settings->get( 'bspp_onboarding_step2_done', 0 );
        $step3_done = (int) $settings->get( 'bspp_onboarding_step3_done', 0 );
        // phpcs:disable WordPress.Security.NonceVerification.Recommended -- onboarding wizard URL state, not form submissions.
        $requested_step = ( isset( $_GET['bspp_onboarding_step'] ) ? absint( wp_unslash( (string) $_GET['bspp_onboarding_step'] ) ) : 0 );
        // Internal indices stay 1 Monetization, 2 Paywall rules (Pro), 3 Stripe, 4 Launch.
        $first_incomplete_step = 1;
        if ( $step1_done === 1 ) {
            $first_incomplete_step = 2;
        }
        // Free build skips Paywall rules (step 2).
        if ( $step1_done === 1 ) {
            $first_incomplete_step = 3;
        }
        if ( $step3_done === 1 ) {
            $first_incomplete_step = 4;
        }
        $step = ( $requested_step >= 1 && $requested_step <= 4 ? $requested_step : $first_incomplete_step );
        // Furthest step the user may open (based on saved progress). Back navigation always allowed within this range.
        $highest_unlocked = 1;
        if ( $step1_done === 1 ) {
            $highest_unlocked = 2;
        }
        if ( $step1_done === 1 ) {
            $highest_unlocked = 3;
        }
        if ( $step3_done === 1 ) {
            $highest_unlocked = 4;
        }
        // Free build never shows Paywall rules; bounce step 2 requests to Stripe.
        if ( $step === 2 ) {
            wp_safe_redirect( admin_url( 'admin.php?page=bobasoftware-publisher-paywall-onboarding&bspp_onboarding_step=3' ) );
            exit;
        }
        if ( $step > $highest_unlocked ) {
            wp_safe_redirect( admin_url( 'admin.php?page=bobasoftware-publisher-paywall-onboarding&bspp_onboarding_step=' . $highest_unlocked ) );
            exit;
        }
        $error_code = ( isset( $_GET['bspp_onboarding_error'] ) ? sanitize_key( wp_unslash( (string) $_GET['bspp_onboarding_error'] ) ) : '' );
        $launched_now = isset( $_GET['bspp_onboarding_launched'] ) && sanitize_key( wp_unslash( (string) $_GET['bspp_onboarding_launched'] ) ) === '1';
        $reset_done = isset( $_GET['bspp_onboarding_reset'] ) && sanitize_key( wp_unslash( (string) $_GET['bspp_onboarding_reset'] ) ) === '1';
        $email_test_result = ( isset( $_GET['bspp_email_test'] ) ? sanitize_key( wp_unslash( (string) $_GET['bspp_email_test'] ) ) : '' );
        // phpcs:enable WordPress.Security.NonceVerification.Recommended
        echo '<div class="wrap sp-onboarding-wrap">';
        echo '<div class="sp-onboarding-hero">';
        echo '<h1>Let\'s set up your paywall</h1>';
        echo '<p>Under 2 minutes. Fine-tune everything later in Access & Monetization and Settings.</p>';
        echo '</div>';
        if ( $reset_done ) {
            echo '<div class="notice notice-success is-dismissible"><p>Onboarding reset. Work through the steps again; launch when you are ready.</p></div>';
        } elseif ( $completed === 1 && $paywall_launched === 1 ) {
            echo '<div class="notice notice-success is-dismissible"><p>Paywall launched. You can review settings anytime.</p></div>';
        } elseif ( $launched_now ) {
            echo '<div class="notice notice-success is-dismissible"><p>Your paywall is ready. It is now active.</p></div>';
        }
        if ( $error_code !== '' ) {
            $msg = 'Please check the information and try again.';
            if ( $error_code === 'no_monetization_selected' ) {
                $msg = 'Turn on Subscription or choose an unlock model to continue.';
                $msg = 'Turn on Subscription to continue.';
            } elseif ( $error_code === 'tip_error' ) {
                $msg = 'Minimum tip cannot be greater than default tip.';
            } elseif ( $error_code === 'stripe_missing' ) {
                $msg = 'Stripe API key and webhook secret are required to continue.';
            }
            echo '<div class="notice notice-error is-dismissible sp-onboarding-error"><p>' . esc_html( $msg ) . '</p></div>';
        }
        $titles = [
            1 => 'Monetization',
            2 => 'Paywall rules',
            3 => 'Connect Stripe',
            4 => 'Launch',
        ];
        $wizard_step_ids = array(
            1,
            2,
            3,
            4
        );
        $wizard_step_ids = array(1, 3, 4);
        echo '<div class="sp-onboarding-steps" role="navigation" aria-label="Onboarding steps">';
        $step_url_base = admin_url( 'admin.php?page=bobasoftware-publisher-paywall-onboarding' );
        $display_num = 0;
        foreach ( $wizard_step_ids as $i ) {
            $display_num++;
            $is_active = $i === $step;
            $is_done = false;
            if ( $i === 1 ) {
                $is_done = $step1_done === 1;
            } elseif ( $i === 2 ) {
                $is_done = $step2_done === 1;
            } elseif ( $i === 3 ) {
                $is_done = $step3_done === 1;
            } elseif ( $i === 4 ) {
                $is_done = $paywall_launched === 1;
            }
            $class = 'sp-onboarding-step-pill';
            if ( $is_active ) {
                $class .= ' is-active';
            } elseif ( $is_done ) {
                $class .= ' is-done';
            }
            $can_nav = $i <= $highest_unlocked;
            if ( !$can_nav ) {
                $class .= ' is-locked';
            }
            $inner = '<span class="sp-onboarding-step-num" aria-hidden="true">' . (int) $display_num . '</span>' . '<span class="sp-onboarding-step-title">' . esc_html( $titles[$i] ) . '</span>';
            if ( $can_nav ) {
                $href = add_query_arg( 'bspp_onboarding_step', (string) $i, $step_url_base );
                if ( $is_active ) {
                    echo '<a class="' . esc_attr( $class ) . '" href="' . esc_url( $href ) . '" aria-current="step">' . wp_kses_post( $inner ) . '</a>';
                } else {
                    /* translators: %d: onboarding step number */
                    $step_link_label = sprintf( __( 'Go to step %d', 'bobasoftware-publisher-paywall' ), $display_num );
                    echo '<a class="' . esc_attr( $class ) . '" href="' . esc_url( $href ) . '" aria-label="' . esc_attr( $step_link_label ) . '">' . wp_kses_post( $inner ) . '</a>';
                }
            } else {
                echo '<span class="' . esc_attr( $class ) . '" role="presentation">' . wp_kses_post( $inner ) . '</span>';
            }
        }
        echo '</div>';
        // Step 1 monetization fields mirror saved settings (not hardcoded). Repository DEFAULTS apply only when a key is missing; reset wizard clears progress flags only.
        $enable_subscription = (int) $settings->get( 'enable_subscription', $d['enable_subscription'] );
        $enable_tip_unlock = 0;
        $enable_article_unlock = 0;
        $subscription_price_monthly = (float) $settings->get( 'subscription_price_monthly', $d['subscription_price_monthly'] );
        $subscription_price_annual = (float) $settings->get( 'subscription_price_annual', $d['subscription_price_annual'] );
        $default_tip_amount = 0.0;
        $minimum_tip_amount = 0.0;
        $article_unlock_price = 0.0;
        $sub_mo_disp = number_format(
            $subscription_price_monthly,
            2,
            '.',
            ''
        );
        $sub_yr_disp = number_format(
            $subscription_price_annual,
            2,
            '.',
            ''
        );
        $tip_def_disp = '0.00';
        $tip_min_disp = '0.00';
        $art_price_disp = '0.00';
        $onboarding_unlock_model = 'tip';
        $free_access_model = 'one_time';
        $free_article_limit = 0;
        $posts_become_paid_after_days = (int) $settings->get( 'posts_become_paid_after_days', $d['posts_become_paid_after_days'] );
        $stripe_api_key = (string) $settings->get( 'stripe_api_key', '' );
        $stripe_webhook_secret = (string) $settings->get( 'stripe_webhook_secret', '' );
        $stripe_connected = trim( $stripe_api_key ) !== '' && trim( $stripe_webhook_secret ) !== '';
        $webhook_url = admin_url( 'admin-post.php?action=bspp_stripe_webhook' );
        // Render step content.
        echo '<div class="sp-onboarding-card">';
        if ( $step === 1 ) {
            echo '<h2>Monetization</h2>';
            echo '<form id="sp-onboarding-step1-form" method="post" action="' . esc_url( admin_url( 'admin-post.php' ) ) . '">';
            echo '<input type="hidden" name="action" value="bspp_onboarding_step1_save" />';
            wp_nonce_field( 'bspp_onboarding_step1_save', 'bspp_onboarding_step1_nonce' );
            echo '<div class="sp-onboarding-subblock">';
            echo '<label class="sp-onboarding-option" style="display:flex;gap:10px;align-items:flex-start;max-width:520px;">';
            echo '<input type="checkbox" id="sp-ob-enable-subscription" name="enable_subscription" value="1" role="switch" ' . checked( $enable_subscription, 1, false ) . ' />';
            echo '<div><div class="sp-onboarding-option-title">Subscription <span style="font-weight:400;color:#6B7280;">(recommended)</span></div>';
            echo '<p class="sp-onboarding-option-desc">Recurring access for your readers.</p></div>';
            echo '</label>';
            echo '<div id="sp-ob-subscription-fields" class="sp-onboarding-field-row" style="margin-top:12px;">';
            echo '<div class="sp-onboarding-field">';
            echo '<label for="sp-ob-sub-monthly">Monthly (USD)</label>';
            echo '<input type="number" id="sp-ob-sub-monthly" name="subscription_price_monthly" min="0" step="0.01" value="' . esc_attr( $sub_mo_disp ) . '" />';
            echo '</div>';
            echo '<div class="sp-onboarding-field">';
            echo '<label for="sp-ob-sub-annual">Annual (USD)</label>';
            echo '<input type="number" id="sp-ob-sub-annual" name="subscription_price_annual" min="0" step="0.01" value="' . esc_attr( $sub_yr_disp ) . '" />';
            echo '</div>';
            echo '</div>';
            echo '</div>';
            echo '<aside class="sp-ob-pro-callout" aria-label="' . esc_attr__( 'More monetization options in Pro', 'bobasoftware-publisher-paywall' ) . '">';
            echo '<p class="sp-ob-pro-callout__title">' . esc_html__( 'More monetization options in Pro', 'bobasoftware-publisher-paywall' ) . '</p>';
            echo '<p class="sp-ob-pro-callout__body">' . esc_html__( 'Publisher Paywall Pro adds tip unlock (readers pay what they want), per-article purchase (fixed price per post), and free-post quotas. All powered by the same Stripe connection.', 'bobasoftware-publisher-paywall' ) . '</p>';
            echo '<a class="sp-ob-pro-callout__link" href="' . esc_url( admin_url( 'admin.php?page=bobasoftware-publisher-paywall-upgrade' ) ) . '" target="_blank" rel="noopener noreferrer">' . esc_html__( 'Learn about Pro →', 'bobasoftware-publisher-paywall' ) . '</a>';
            echo '</aside>';
            $step1_recommended = [
                'subscription_price_monthly' => (float) $d['subscription_price_monthly'],
                'subscription_price_annual'  => (float) $d['subscription_price_annual'],
            ];
            wp_add_inline_script( 'bspp-admin-scripts', 'var spOnboardingStep1Recommended = ' . wp_json_encode( $step1_recommended, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT ) . ';', 'before' );
            echo '<p class="sp-onboarding-inline-note" style="margin-top:14px;">';
            echo esc_html__( 'These amounts are loaded from your saved settings. Reset setup wizard only clears progress, not Access & Monetization.', 'bobasoftware-publisher-paywall' );
            echo ' ';
            echo '<button type="button" class="button" id="sp-ob-use-recommended">' . esc_html__( 'Use recommended defaults', 'bobasoftware-publisher-paywall' ) . '</button>';
            echo '</p>';
            echo '<div class="sp-onboarding-actions">';
            echo '<button type="submit" class="button button-primary">Continue</button>';
            echo '</div>';
            echo '</form>';
        } elseif ( $step === 2 ) {
        } elseif ( $step === 3 ) {
            echo '<div class="sp-onboarding-stripe-setup">';
            $status_class = ( $stripe_connected ? 'sp-stripe-connect-status is-connected' : 'sp-stripe-connect-status' );
            echo '<div class="sp-stripe-setup-card__header">';
            echo '<h2 class="sp-stripe-setup-card__title">💳 Connect Stripe</h2>';
            echo '<div id="sp-onboarding-stripe-status" class="' . esc_attr( $status_class ) . '" role="status" aria-live="polite"' . ' data-label-connected="' . esc_attr( __( 'Connected', 'bobasoftware-publisher-paywall' ) ) . '"' . ' data-label-not="' . esc_attr( __( 'Not connected', 'bobasoftware-publisher-paywall' ) ) . '">';
            echo '<div class="sp-stripe-connect-status__row"><span class="sp-stripe-connect-status__dot" aria-hidden="true"></span>';
            echo '<span class="sp-stripe-connect-status__label">' . (( $stripe_connected ? esc_html__( 'Connected', 'bobasoftware-publisher-paywall' ) : esc_html__( 'Not connected', 'bobasoftware-publisher-paywall' ) )) . '</span></div>';
            echo '<span class="sp-stripe-connect-status__sub">' . esc_html__( 'Payments are ready', 'bobasoftware-publisher-paywall' ) . '</span>';
            echo '</div></div>';
            echo '<p class="sp-stripe-setup-card__lede">' . esc_html__( 'Takes about 2 minutes.', 'bobasoftware-publisher-paywall' ) . '</p>';
            echo '<form id="sp-onboarding-step3-form" method="post" action="' . esc_url( admin_url( 'admin-post.php' ) ) . '">';
            echo '<input type="hidden" name="action" value="bspp_onboarding_step3_save" />';
            wp_nonce_field( 'bspp_onboarding_step3_save', 'bspp_onboarding_step3_nonce' );
            echo '<div class="sp-stripe-step">';
            echo '<div class="sp-stripe-step__marker" aria-hidden="true">1</div>';
            echo '<div class="sp-stripe-step__content">';
            echo '<p class="sp-stripe-step__title">' . esc_html__( 'Copy webhook URL', 'bobasoftware-publisher-paywall' ) . '</p>';
            echo '<div class="sp-stripe-step__field">';
            echo '<label for="sp-onboarding-stripe-webhook-url">' . esc_html__( 'Webhook URL', 'bobasoftware-publisher-paywall' ) . '</label>';
            echo '<div class="sp-stripe-webhook-row">';
            echo '<input type="text" id="sp-onboarding-stripe-webhook-url" class="regular-text" value="' . esc_attr( $webhook_url ) . '" readonly autocomplete="off" />';
            echo '<button type="button" class="button" id="sp-onboarding-copy-webhook-url" aria-label="' . esc_attr__( 'Copy webhook URL to clipboard', 'bobasoftware-publisher-paywall' ) . '">' . esc_html__( 'Copy', 'bobasoftware-publisher-paywall' ) . '</button>';
            echo '</div></div>';
            echo '<p class="sp-stripe-step__helper">' . esc_html__( 'Copy this URL. You will use it in Stripe.', 'bobasoftware-publisher-paywall' ) . '</p>';
            echo '</div></div>';
            echo '<div class="sp-stripe-step sp-stripe-step--textonly">';
            echo '<div class="sp-stripe-step__marker" aria-hidden="true">2</div>';
            echo '<div class="sp-stripe-step__content">';
            echo '<p class="sp-stripe-step__title">' . esc_html__( 'Create webhook in Stripe', 'bobasoftware-publisher-paywall' ) . '</p>';
            echo '<p class="sp-stripe-step__helper">' . esc_html__( 'In Stripe, go to Developers → Webhooks → Add destination. Select these three events, then paste the URL from Step 1 into the Endpoint URL field.', 'bobasoftware-publisher-paywall' ) . '</p>';
            echo '<ul class="sp-stripe-step__helper">';
            echo '<li>' . esc_html( 'checkout.session.completed' ) . '</li>';
            echo '<li>' . esc_html( 'customer.subscription.updated' ) . '</li>';
            echo '<li>' . esc_html( 'customer.subscription.deleted' ) . '</li>';
            echo '</ul>';
            echo '</div></div>';
            echo '<div class="sp-stripe-step">';
            echo '<div class="sp-stripe-step__marker" aria-hidden="true">3</div>';
            echo '<div class="sp-stripe-step__content">';
            echo '<p class="sp-stripe-step__title">' . esc_html__( 'Paste webhook secret', 'bobasoftware-publisher-paywall' ) . '</p>';
            echo '<div class="sp-stripe-step__field">';
            echo '<label for="stripe_webhook_secret">' . esc_html__( 'Stripe webhook secret', 'bobasoftware-publisher-paywall' ) . '</label>';
            $wh_has_value = trim( (string) $stripe_webhook_secret ) !== '';
            echo '<div class="sp-secret-field">';
            echo '<input type="text" class="regular-text sp-secret-field__masked" value="' . esc_attr( $this->bspp_mask_secret( $stripe_webhook_secret ) ) . '" readonly tabindex="-1" aria-hidden="true"' . (( $wh_has_value ? '' : ' style="display:none;"' )) . ' />';
            echo '<input type="text" id="stripe_webhook_secret" name="stripe_webhook_secret" value="' . esc_attr( $stripe_webhook_secret ) . '" class="regular-text sp-secret-field__real" placeholder="whsec_..." autocomplete="off" spellcheck="false"' . (( $wh_has_value ? ' style="display:none;"' : '' )) . ' />';
            echo '<button type="button" class="button-link sp-secret-field__toggle" aria-pressed="false"' . (( $wh_has_value ? '' : ' style="display:none;"' )) . '>' . esc_html__( 'Reveal', 'bobasoftware-publisher-paywall' ) . '</button>';
            echo '</div>';
            echo '</div>';
            echo '<p class="sp-stripe-step__helper">' . esc_html__( 'Paste the signing secret from Stripe (starts with', 'bobasoftware-publisher-paywall' ) . ' <code>whsec_</code>).</p>';
            echo '</div></div>';
            echo '<div class="sp-stripe-step">';
            echo '<div class="sp-stripe-step__marker" aria-hidden="true">4</div>';
            echo '<div class="sp-stripe-step__content">';
            echo '<p class="sp-stripe-step__title">' . esc_html__( 'Add Stripe API key', 'bobasoftware-publisher-paywall' ) . '</p>';
            echo '<div class="sp-stripe-step__field">';
            echo '<label for="stripe_api_key">' . esc_html__( 'Stripe API key (secret)', 'bobasoftware-publisher-paywall' ) . '</label>';
            $key_has_value = trim( (string) $stripe_api_key ) !== '';
            echo '<div class="sp-secret-field">';
            echo '<input type="text" class="regular-text sp-secret-field__masked" value="' . esc_attr( $this->bspp_mask_secret( $stripe_api_key ) ) . '" readonly tabindex="-1" aria-hidden="true"' . (( $key_has_value ? '' : ' style="display:none;"' )) . ' />';
            echo '<input type="text" id="stripe_api_key" name="stripe_api_key" value="' . esc_attr( $stripe_api_key ) . '" class="regular-text sp-secret-field__real" placeholder="sk_..." autocomplete="off" spellcheck="false"' . (( $key_has_value ? ' style="display:none;"' : '' )) . ' />';
            echo '<button type="button" class="button-link sp-secret-field__toggle" aria-pressed="false"' . (( $key_has_value ? '' : ' style="display:none;"' )) . '>' . esc_html__( 'Reveal', 'bobasoftware-publisher-paywall' ) . '</button>';
            echo '</div>';
            echo '</div>';
            echo '<p class="sp-stripe-step__helper">' . esc_html__( 'Find this in Stripe → Developers → API keys. Paste your secret key (starts with', 'bobasoftware-publisher-paywall' ) . ' <code>sk_</code>).</p>';
            echo '</div></div>';
            echo '<div class="sp-onboarding-actions">';
            $stripe_back_step = 2;
            $stripe_back_step = 1;
            echo '<a class="button" href="' . esc_url( admin_url( 'admin.php?page=bobasoftware-publisher-paywall-onboarding&bspp_onboarding_step=' . (int) $stripe_back_step ) ) . '">' . esc_html__( 'Back', 'bobasoftware-publisher-paywall' ) . '</a>';
            echo '<button type="submit" class="button button-primary">' . esc_html__( 'Continue', 'bobasoftware-publisher-paywall' ) . '</button>';
            echo '</div>';
            echo '</form>';
            echo '</div>';
            $this->bspp_render_onboarding_email_section( $email_test_result );
        } else {
            // Step 4
            echo '<h2>Launch</h2>';
            echo '<p class="sp-onboarding-inline-note" style="margin-bottom:14px;">When you launch, readers will start seeing premium content.</p>';
            if ( $enable_subscription === 1 ) {
                $subscription_summary_line = sprintf( 
                    /* translators: 1: monthly subscription price, 2: annual subscription price */
                    __( 'Subscription on — $%1$s/mo, $%2$s/yr', 'bobasoftware-publisher-paywall' ),
                    $sub_mo_disp,
                    $sub_yr_disp
                 );
            } else {
                $subscription_summary_line = __( 'Subscription off', 'bobasoftware-publisher-paywall' );
            }
            $timing_summary_line = sprintf( 
                /* translators: %d: number of days */
                __( 'Premium after %d days from publish', 'bobasoftware-publisher-paywall' ),
                (int) $posts_become_paid_after_days
             );
            $payments_summary_line = ( $stripe_connected ? __( 'Stripe connected', 'bobasoftware-publisher-paywall' ) : __( 'Stripe not connected', 'bobasoftware-publisher-paywall' ) );
            echo '<div class="sp-onboarding-summary">';
            echo '<div class="sp-onboarding-summary-group sp-onboarding-summary-group--launch">';
            echo '<h3 class="sp-onboarding-summary-group__title">' . esc_html__( 'Launching now', 'bobasoftware-publisher-paywall' ) . '</h3>';
            echo '<div class="sp-onboarding-summary-section"><h3>' . esc_html__( 'Monetization', 'bobasoftware-publisher-paywall' ) . '</h3>';
            echo '<p>' . esc_html( $subscription_summary_line ) . '</p></div>';
            echo '<div class="sp-onboarding-summary-section"><h3>' . esc_html__( 'Timing', 'bobasoftware-publisher-paywall' ) . '</h3>';
            echo '<p>' . esc_html( $timing_summary_line ) . '</p></div>';
            echo '<div class="sp-onboarding-summary-section"><h3>' . esc_html__( 'Payments', 'bobasoftware-publisher-paywall' ) . '</h3>';
            echo '<p>' . esc_html( $payments_summary_line ) . '</p></div>';
            echo '</div>';
            echo '</div>';
            echo '<form method="post" action="' . esc_url( admin_url( 'admin-post.php' ) ) . '">';
            echo '<input type="hidden" name="action" value="bspp_onboarding_launch" />';
            wp_nonce_field( 'bspp_onboarding_launch', 'bspp_onboarding_launch_nonce' );
            echo '<div class="sp-onboarding-actions">';
            echo '<a class="button" href="' . esc_url( admin_url( 'admin.php?page=bobasoftware-publisher-paywall-onboarding&bspp_onboarding_step=3' ) ) . '">' . esc_html__( 'Back', 'bobasoftware-publisher-paywall' ) . '</a>';
            echo '<div class="sp-onboarding-actions--launch">';
            echo '<button type="submit" class="sp-onboarding-launch-primary">' . esc_html__( 'Launch paywall', 'bobasoftware-publisher-paywall' ) . '</button>';
            echo '<a class="sp-onboarding-launch-trial" href="' . esc_url( bspp_fs()->get_trial_url() ) . '">';
            echo '<span class="sp-onboarding-launch-trial__label">' . esc_html__( 'Start free trial', 'bobasoftware-publisher-paywall' ) . '</span>';
            echo '<span class="sp-onboarding-launch-trial__sub">' . esc_html__( '14 days, no credit card', 'bobasoftware-publisher-paywall' ) . '</span>';
            echo '</a>';
            echo '</div>';
            echo '</div>';
            echo '</form>';
        }
        echo '</div>';
        // card
        $allow_reset = (bool) apply_filters( 'bspp_allow_onboarding_reset', true );
        if ( $allow_reset && ($paywall_launched === 1 || $completed === 1) ) {
            echo '<div class="sp-onboarding-reset" style="margin-top:20px;padding-top:16px;border-top:1px solid #E5E7EB;max-width:920px;">';
            echo '<p class="sp-onboarding-inline-note">Testing again? This clears onboarding progress only. Access &amp; Monetization and Stripe values are unchanged. The paywall pauses until you launch again.</p>';
            echo '<form method="post" action="' . esc_url( admin_url( 'admin-post.php' ) ) . '" onsubmit="return confirm(\'Reset onboarding progress? The paywall will pause until you launch again.\');">';
            echo '<input type="hidden" name="action" value="bspp_onboarding_reset" />';
            wp_nonce_field( 'bspp_onboarding_reset', 'bspp_onboarding_reset_nonce' );
            echo '<p><button type="submit" class="button">Reset onboarding</button></p>';
            echo '</form>';
            echo '</div>';
        }
        echo '</div>';
        // wrap
    }

    /**
     * Clear onboarding progress flags so the wizard can be run again (e.g. local QA).
     * Does not remove monetization or Stripe settings.
     */
    public function handle_onboarding_reset() {
        if ( !current_user_can( 'manage_options' ) ) {
            wp_die( 'Unauthorized' );
        }
        if ( !apply_filters( 'bspp_allow_onboarding_reset', true ) ) {
            wp_die( 'Unauthorized' );
        }
        if ( !isset( $_POST['bspp_onboarding_reset_nonce'] ) || !wp_verify_nonce( sanitize_text_field( wp_unslash( (string) $_POST['bspp_onboarding_reset_nonce'] ) ), 'bspp_onboarding_reset' ) ) {
            wp_die( 'Invalid request' );
        }
        $settings = new BSPP_Settings_Repository();
        foreach ( [
            'bspp_onboarding_completed',
            'bspp_paywall_launched',
            'bspp_onboarding_step1_done',
            'bspp_onboarding_step2_done',
            'bspp_onboarding_step3_done'
        ] as $key ) {
            $settings->set( $key, 0 );
        }
        wp_safe_redirect( admin_url( 'admin.php?page=bobasoftware-publisher-paywall-onboarding&bspp_onboarding_step=1&bspp_onboarding_reset=1' ) );
        exit;
    }

    public function handle_onboarding_step1_save() {
        if ( !current_user_can( 'manage_options' ) ) {
            wp_die( 'Unauthorized' );
        }
        if ( !isset( $_POST['bspp_onboarding_step1_nonce'] ) || !wp_verify_nonce( sanitize_text_field( wp_unslash( (string) $_POST['bspp_onboarding_step1_nonce'] ) ), 'bspp_onboarding_step1_save' ) ) {
            wp_die( 'Invalid request' );
        }
        $settings = new BSPP_Settings_Repository();
        $d = BSPP_Settings_Repository::defaults();
        $enable_subscription = ( isset( $_POST['enable_subscription'] ) ? 1 : 0 );
        if ( $enable_subscription === 1 ) {
            $subscription_price_monthly = ( isset( $_POST['subscription_price_monthly'] ) ? floatval( wp_unslash( $_POST['subscription_price_monthly'] ) ) : (float) $settings->get( 'subscription_price_monthly', $d['subscription_price_monthly'] ) );
            $subscription_price_annual = ( isset( $_POST['subscription_price_annual'] ) ? floatval( wp_unslash( $_POST['subscription_price_annual'] ) ) : (float) $settings->get( 'subscription_price_annual', $d['subscription_price_annual'] ) );
        } else {
            $subscription_price_monthly = (float) $settings->get( 'subscription_price_monthly', $d['subscription_price_monthly'] );
            $subscription_price_annual = (float) $settings->get( 'subscription_price_annual', $d['subscription_price_annual'] );
        }
        $unlock = 'tip';
        $enable_tip_unlock = 0;
        $enable_article_unlock = 0;
        $default_tip_amount = (float) $settings->get( 'default_tip_amount', ( isset( $d['default_tip_amount'] ) ? $d['default_tip_amount'] : 0 ) );
        $minimum_tip_amount = (float) $settings->get( 'minimum_tip_amount', ( isset( $d['minimum_tip_amount'] ) ? $d['minimum_tip_amount'] : 0 ) );
        $article_unlock_price = (float) $settings->get( 'article_unlock_price', ( isset( $d['article_unlock_price'] ) ? $d['article_unlock_price'] : 0 ) );
        $subscription_price_monthly = max( 0, $subscription_price_monthly );
        $subscription_price_annual = max( 0, $subscription_price_annual );
        $default_tip_amount = max( 0, round( (float) $default_tip_amount, 2 ) );
        $minimum_tip_amount = max( 0, round( (float) $minimum_tip_amount, 2 ) );
        $article_unlock_price = max( 0, $article_unlock_price );
        if ( $enable_subscription !== 1 && $enable_tip_unlock !== 1 && $enable_article_unlock !== 1 ) {
            wp_safe_redirect( admin_url( 'admin.php?page=bobasoftware-publisher-paywall-onboarding&bspp_onboarding_step=1&bspp_onboarding_error=no_monetization_selected' ) );
            exit;
        }
        $settings->set( 'enable_subscription', $enable_subscription );
        $settings->set( 'subscription_price_monthly', $subscription_price_monthly );
        $settings->set( 'subscription_price_annual', $subscription_price_annual );
        $settings->set( 'bspp_onboarding_step1_done', 1 );
        $next_step = 2;
        // Free build skips Paywall rules; mark that step complete and go to Stripe.
        $settings->set( 'bspp_onboarding_step2_done', 1 );
        $next_step = 3;
        wp_safe_redirect( admin_url( 'admin.php?page=bobasoftware-publisher-paywall-onboarding&bspp_onboarding_step=' . (int) $next_step ) );
        exit;
    }

    public function handle_onboarding_step2_save() {
        if ( !current_user_can( 'manage_options' ) ) {
            wp_die( 'Unauthorized' );
        }
        if ( !isset( $_POST['bspp_onboarding_step2_nonce'] ) || !wp_verify_nonce( sanitize_text_field( wp_unslash( (string) $_POST['bspp_onboarding_step2_nonce'] ) ), 'bspp_onboarding_step2_save' ) ) {
            wp_die( 'Invalid request' );
        }
        $settings = new BSPP_Settings_Repository();
        $step1_done = (int) $settings->get( 'bspp_onboarding_step1_done', 0 );
        if ( $step1_done !== 1 ) {
            wp_safe_redirect( admin_url( 'admin.php?page=bobasoftware-publisher-paywall-onboarding&bspp_onboarding_step=1' ) );
            exit;
        }
        $posts_become_paid_after_days = ( isset( $_POST['posts_become_paid_after_days'] ) ? absint( wp_unslash( (string) $_POST['posts_become_paid_after_days'] ) ) : 0 );
        $posts_become_paid_after_days = max( 0, $posts_become_paid_after_days );
        $settings->set( 'posts_become_paid_after_days', $posts_become_paid_after_days );
        $settings->set( 'bspp_onboarding_step2_done', 1 );
        wp_safe_redirect( admin_url( 'admin.php?page=bobasoftware-publisher-paywall-onboarding&bspp_onboarding_step=3' ) );
        exit;
    }

    public function handle_onboarding_step3_save() {
        if ( !current_user_can( 'manage_options' ) ) {
            wp_die( 'Unauthorized' );
        }
        if ( !isset( $_POST['bspp_onboarding_step3_nonce'] ) || !wp_verify_nonce( sanitize_text_field( wp_unslash( (string) $_POST['bspp_onboarding_step3_nonce'] ) ), 'bspp_onboarding_step3_save' ) ) {
            wp_die( 'Invalid request' );
        }
        $settings = new BSPP_Settings_Repository();
        $step2_done = (int) $settings->get( 'bspp_onboarding_step2_done', 0 );
        if ( $step2_done !== 1 ) {
            $fallback_step = 2;
            $fallback_step = 1;
            wp_safe_redirect( admin_url( 'admin.php?page=bobasoftware-publisher-paywall-onboarding&bspp_onboarding_step=' . (int) $fallback_step ) );
            exit;
        }
        $stripe_api_key = ( isset( $_POST['stripe_api_key'] ) ? sanitize_text_field( wp_unslash( (string) $_POST['stripe_api_key'] ) ) : '' );
        $stripe_webhook_secret = ( isset( $_POST['stripe_webhook_secret'] ) ? sanitize_text_field( wp_unslash( (string) $_POST['stripe_webhook_secret'] ) ) : '' );
        $stripe_api_key = trim( $stripe_api_key );
        $stripe_webhook_secret = trim( $stripe_webhook_secret );
        if ( $stripe_api_key === '' || $stripe_webhook_secret === '' ) {
            wp_safe_redirect( admin_url( 'admin.php?page=bobasoftware-publisher-paywall-onboarding&bspp_onboarding_step=3&bspp_onboarding_error=stripe_missing' ) );
            exit;
        }
        $settings->set( 'stripe_api_key', $stripe_api_key );
        $settings->set( 'stripe_webhook_secret', $stripe_webhook_secret );
        $settings->set( 'bspp_onboarding_step3_done', 1 );
        wp_safe_redirect( admin_url( 'admin.php?page=bobasoftware-publisher-paywall-onboarding&bspp_onboarding_step=4' ) );
        exit;
    }

    public function handle_onboarding_launch() {
        if ( !current_user_can( 'manage_options' ) ) {
            wp_die( 'Unauthorized' );
        }
        if ( !isset( $_POST['bspp_onboarding_launch_nonce'] ) || !wp_verify_nonce( sanitize_text_field( wp_unslash( (string) $_POST['bspp_onboarding_launch_nonce'] ) ), 'bspp_onboarding_launch' ) ) {
            wp_die( 'Invalid request' );
        }
        $settings = new BSPP_Settings_Repository();
        $step1_done = (int) $settings->get( 'bspp_onboarding_step1_done', 0 );
        $step2_done = (int) $settings->get( 'bspp_onboarding_step2_done', 0 );
        $step3_done = (int) $settings->get( 'bspp_onboarding_step3_done', 0 );
        if ( $step1_done !== 1 || $step2_done !== 1 || $step3_done !== 1 ) {
            wp_safe_redirect( admin_url( 'admin.php?page=bobasoftware-publisher-paywall-onboarding&bspp_onboarding_step=1' ) );
            exit;
        }
        $stripe_api_key = (string) $settings->get( 'stripe_api_key', '' );
        $stripe_webhook_secret = (string) $settings->get( 'stripe_webhook_secret', '' );
        $stripe_connected = trim( $stripe_api_key ) !== '' && trim( $stripe_webhook_secret ) !== '';
        if ( !$stripe_connected ) {
            wp_safe_redirect( admin_url( 'admin.php?page=bobasoftware-publisher-paywall-onboarding&bspp_onboarding_step=3' ) );
            exit;
        }
        $settings->set( 'bspp_onboarding_completed', 1 );
        $settings->set( 'bspp_paywall_launched', 1 );
        wp_safe_redirect( admin_url( 'admin.php?page=bobasoftware-publisher-paywall&bspp_onboarding_launch_done=1' ) );
        exit;
    }

    private function normalize_membership_status( $status ) {
        $status = sanitize_key( (string) $status );
        $labels = [
            'active'     => 'Active',
            'canceled'   => 'Canceled',
            'cancelled'  => 'Canceled',
            'past_due'   => 'Past due',
            'expired'    => 'Expired',
            'incomplete' => 'Incomplete',
            'trialing'   => 'Trialing',
            'succeeded'  => 'Succeeded',
            'refunded'   => 'Refunded',
            'pending'    => 'Pending',
        ];
        if ( isset( $labels[$status] ) ) {
            return $labels[$status];
        }
        if ( $status === '' ) {
            return 'Free';
        }
        return ucwords( str_replace( '_', ' ', $status ) );
    }

    /**
     * Human-readable purchase type label (Payments table, Tools webhook Product column).
     *
     * @param string $type Stored purchase row type key.
     */
    private function get_purchase_type_display_label( $type ) {
        $type = sanitize_key( (string) $type );
        $labels = [
            'subscription_monthly' => __( 'Subscription (monthly)', 'bobasoftware-publisher-paywall' ),
            'subscription_annual'  => __( 'Subscription (annual)', 'bobasoftware-publisher-paywall' ),
            'subscription'         => __( 'Subscription', 'bobasoftware-publisher-paywall' ),
            'tip_unlock'           => __( 'Tip', 'bobasoftware-publisher-paywall' ),
            'article_unlock'       => __( 'Per-article purchase', 'bobasoftware-publisher-paywall' ),
        ];
        if ( isset( $labels[$type] ) ) {
            return $labels[$type];
        }
        if ( $type === '' ) {
            return __( 'Unknown', 'bobasoftware-publisher-paywall' );
        }
        return ucwords( str_replace( '_', ' ', $type ) );
    }

    private function format_purchase_type_label( $type ) {
        return $this->get_purchase_type_display_label( $type );
    }

    private function get_members_admin_rows() {
        global $wpdb;
        $users_table = $wpdb->users;
        $memberships_table = $wpdb->prefix . 'bobasoftware_pp_memberships';
        $entitlements_table = $wpdb->prefix . 'bobasoftware_pp_entitlements';
        $usage_table = $wpdb->prefix . 'bobasoftware_pp_usage_meters';
        $purchases_table = $wpdb->prefix . 'bobasoftware_pp_purchases';
        // Readers with any Bobasoftware Publisher Paywall row (membership, free-quota meter, or purchase).
        $sql = "SELECT\n                    u.ID AS user_id,\n                    u.user_email AS email,\n                    u.display_name AS display_name,\n                    m_latest.id AS membership_id,\n                    m_latest.status AS membership_status,\n                    m_latest.plan AS membership_plan,\n                    m_latest.current_period_end AS current_period_end,\n                    m_latest.stripe_subscription_id AS stripe_subscription_id,\n                    m_latest.cancel_at_period_end AS cancel_at_period_end,\n                    COALESCE(fm.free_posts_used, 0) AS unlock_count,\n                    ent.last_unlock_at AS last_unlock_at\n                FROM {$users_table} u\n                INNER JOIN (\n                    SELECT user_id FROM {$memberships_table} WHERE user_id IS NOT NULL\n                    UNION\n                    SELECT user_id FROM {$usage_table} WHERE user_id IS NOT NULL\n                    UNION\n                    SELECT user_id FROM {$purchases_table} WHERE user_id IS NOT NULL\n                ) sp ON sp.user_id = u.ID\n                LEFT JOIN (\n                    SELECT m.id, m.user_id, m.status, m.plan, m.current_period_end, m.created_at, m.stripe_subscription_id, m.cancel_at_period_end\n                    FROM {$memberships_table} m\n                    INNER JOIN (\n                        SELECT user_id, MAX(id) AS latest_id\n                        FROM {$memberships_table}\n                        GROUP BY user_id\n                    ) latest ON latest.latest_id = m.id\n                ) m_latest ON m_latest.user_id = u.ID\n                LEFT JOIN (\n                    SELECT user_id, MAX(created_at) AS last_unlock_at\n                    FROM {$entitlements_table}\n                    WHERE type IN ('article_unlock', 'tip_unlock')\n                    GROUP BY user_id\n                ) ent ON ent.user_id = u.ID\n                LEFT JOIN (\n                    SELECT user_id, SUM(used_count) AS free_posts_used\n                    FROM {$usage_table}\n                    WHERE meter_key = 'one_time'\n                       OR meter_key LIKE 'monthly_%'\n                    GROUP BY user_id\n                ) fm ON fm.user_id = u.ID\n                LEFT JOIN (\n                    SELECT user_id, MAX(created_at) AS last_purchased_at\n                    FROM {$purchases_table}\n                    WHERE user_id IS NOT NULL\n                    GROUP BY user_id\n                ) pur ON pur.user_id = u.ID\n                LEFT JOIN (\n                    SELECT user_id, MAX(updated_at) AS last_usage_at\n                    FROM {$usage_table}\n                    GROUP BY user_id\n                ) umat ON umat.user_id = u.ID\n                ORDER BY COALESCE(m_latest.created_at, ent.last_unlock_at, pur.last_purchased_at, umat.last_usage_at, u.user_registered) DESC";
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- real-time member lookup.
        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- table names from $wpdb are safe.
        $rows = $wpdb->get_results( $sql );
        return ( is_array( $rows ) ? $rows : [] );
    }

    private function get_payments_pending_count() {
        global $wpdb;
        $table = $wpdb->prefix . 'bobasoftware_pp_purchases';
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- real-time payment lookup.
        // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name is safe.
        return (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$table} WHERE status = 'pending'" );
    }

    private function get_payments_admin_rows( $offset, $per_page ) {
        global $wpdb;
        $offset = max( 0, (int) $offset );
        $per_page = max( 1, (int) $per_page );
        $table = $wpdb->prefix . 'bobasoftware_pp_purchases';
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- real-time payment lookup.
        // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name is safe.
        $total = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$table} WHERE status != 'pending'" );
        $sql = $wpdb->prepare( "SELECT id, user_id, email, type, amount, status, stripe_session_id, stripe_payment_intent_id, stripe_invoice_id, created_at\n             FROM {$table}\n             WHERE status != 'pending'\n             ORDER BY created_at DESC, id DESC\n             LIMIT %d OFFSET %d", $per_page, $offset );
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- real-time payment lookup.
        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- query prepared via $wpdb->prepare above.
        $rows = $wpdb->get_results( $sql );
        return [
            'rows'  => ( is_array( $rows ) ? $rows : [] ),
            'total' => $total,
        ];
    }

    /**
     * Abandoned Stripe Checkout sessions (pending purchase rows) for the Payments admin toggle.
     *
     * @return object[]
     */
    private function get_payments_admin_pending_rows() {
        global $wpdb;
        $table = $wpdb->prefix . 'bobasoftware_pp_purchases';
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- real-time payment lookup.
        // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name is safe.
        $rows = $wpdb->get_results( "SELECT id, user_id, email, type, amount, status, stripe_session_id, stripe_payment_intent_id, stripe_invoice_id, created_at\n             FROM {$table}\n             WHERE status = 'pending'\n             ORDER BY created_at DESC, id DESC" );
        return ( is_array( $rows ) ? $rows : [] );
    }

    private function render_payments_admin_table_header() {
        echo '<thead><tr>';
        echo '<th>' . esc_html__( 'Date', 'bobasoftware-publisher-paywall' ) . '</th>';
        echo '<th>' . esc_html__( 'User/Email', 'bobasoftware-publisher-paywall' ) . '</th>';
        echo '<th>' . esc_html__( 'Type', 'bobasoftware-publisher-paywall' ) . '</th>';
        echo '<th>' . esc_html__( 'Amount', 'bobasoftware-publisher-paywall' ) . '</th>';
        echo '<th title="' . esc_attr__( 'This is the transaction ID from the reader\'s Stripe receipt. Use it to match a reader\'s payment to this record.', 'bobasoftware-publisher-paywall' ) . '">' . esc_html__( 'Order reference', 'bobasoftware-publisher-paywall' ) . '</th>';
        echo '<th title="' . esc_attr__( 'This is the invoice number from the reader\'s Stripe receipt. Use it to match a reader\'s subscription payment to this record.', 'bobasoftware-publisher-paywall' ) . '">' . esc_html__( 'Invoice ID', 'bobasoftware-publisher-paywall' ) . '</th>';
        echo '<th>' . esc_html__( 'Status', 'bobasoftware-publisher-paywall' ) . '</th>';
        echo '<th>' . esc_html__( 'Actions', 'bobasoftware-publisher-paywall' ) . '</th>';
        echo '</tr></thead>';
    }

    /**
     * @param object[] $rows
     */
    private function render_payments_admin_table( $rows, $muted = false ) {
        $table_class = 'widefat striped';
        if ( $muted ) {
            $table_class .= ' sp-payments-table--abandoned';
        }
        echo '<table class="' . esc_attr( $table_class ) . '">';
        $this->render_payments_admin_table_header();
        echo '<tbody>';
        foreach ( $rows as $row ) {
            $this->render_payments_admin_table_row( $row, $muted );
        }
        echo '</tbody></table>';
    }

    /**
     * @param object $row
     */
    private function render_payments_admin_table_row( $row, $muted = false ) {
        $created_at = ( is_string( $row->created_at ?? '' ) ? $row->created_at : '' );
        $date_label = ( $created_at !== '' ? mysql2date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $created_at ) : '—' );
        $user_email = ( is_string( $row->email ?? '' ) ? $row->email : '' );
        $user_label = ( $user_email !== '' ? $user_email : 'User #' . (int) ($row->user_id ?? 0) );
        $purchase_type = sanitize_key( (string) ($row->type ?? '') );
        $is_subscription = in_array( $purchase_type, ['subscription_monthly', 'subscription_annual', 'subscription'], true );
        $type_label = $this->format_purchase_type_label( $row->type ?? '' );
        $amount = ( isset( $row->amount ) ? (float) $row->amount : 0.0 );
        $raw_purchase_status = sanitize_key( (string) ($row->status ?? '') );
        $status_label = $this->normalize_membership_status( $row->status ?? '' );
        $stripe_session_id = ( isset( $row->stripe_session_id ) ? (string) $row->stripe_session_id : '' );
        $db_pi = ( isset( $row->stripe_payment_intent_id ) ? trim( (string) $row->stripe_payment_intent_id ) : '' );
        $session_meta = $this->get_payments_row_checkout_session_meta( $stripe_session_id );
        $use_freemius_refund = (bool) apply_filters(
            'bspp_purchase_use_freemius_refund',
            false,
            $row,
            $session_meta
        );
        $fs_dash = apply_filters(
            'bspp_freemius_refund_dashboard_url',
            'https://dashboard.freemius.com/',
            $row,
            $session_meta
        );
        if ( !is_string( $fs_dash ) || $fs_dash === '' ) {
            $fs_dash = 'https://dashboard.freemius.com/';
        }
        $pi_raw = ( $db_pi !== '' && strpos( $db_pi, 'pi_' ) === 0 ? $db_pi : $session_meta['payment_intent'] );
        $order_ref = ( is_string( $pi_raw ) && strpos( $pi_raw, 'pi_' ) === 0 ? $pi_raw : '—' );
        $db_inv = ( isset( $row->stripe_invoice_id ) ? trim( (string) $row->stripe_invoice_id ) : '' );
        $inv_for_row = ( $db_inv !== '' && strpos( $db_inv, 'in_' ) === 0 ? $db_inv : '' );
        if ( $inv_for_row === '' && $is_subscription && !empty( $session_meta['invoice_id'] ) ) {
            $tmp_inv = trim( (string) $session_meta['invoice_id'] );
            if ( $tmp_inv !== '' && strpos( $tmp_inv, 'in_' ) === 0 ) {
                $inv_for_row = $tmp_inv;
            }
        }
        $invoice_cell = ( $is_subscription && $inv_for_row !== '' ? $inv_for_row : '—' );
        $show_refund = $raw_purchase_status === 'succeeded' && in_array( $purchase_type, ['article_unlock', 'tip_unlock'], true ) && is_string( $pi_raw ) && strpos( $pi_raw, 'pi_' ) === 0 && !$use_freemius_refund;
        $show_sub_refund = $raw_purchase_status === 'succeeded' && $is_subscription && $inv_for_row !== '' && !$use_freemius_refund;
        $show_freemius_refund_help = $raw_purchase_status === 'succeeded' && $use_freemius_refund && in_array( $purchase_type, [
            'article_unlock',
            'tip_unlock',
            'subscription_monthly',
            'subscription_annual',
            'subscription'
        ], true );
        if ( $muted ) {
            echo '<tr class="sp-payments-row--abandoned">';
        } else {
            echo '<tr>';
        }
        echo '<td>' . esc_html( $date_label ) . '</td>';
        echo '<td>' . esc_html( $user_label ) . '</td>';
        echo '<td>';
        echo esc_html( $type_label );
        if ( ($purchase_type === 'article_unlock' || $purchase_type === 'tip_unlock') && !empty( $session_meta['post_id'] ) ) {
            $unlock_title = get_the_title( (int) $session_meta['post_id'] );
            if ( is_string( $unlock_title ) && $unlock_title !== '' ) {
                echo '<br /><span class="description">' . esc_html( $unlock_title ) . '</span>';
            }
        }
        echo '</td>';
        echo '<td>$' . esc_html( number_format(
            $amount,
            2,
            '.',
            ''
        ) ) . '</td>';
        echo '<td><code>' . esc_html( $order_ref ) . '</code></td>';
        echo '<td><code>' . esc_html( $invoice_cell ) . '</code></td>';
        echo '<td>' . esc_html( $status_label ) . '</td>';
        echo '<td>';
        if ( $show_refund ) {
            $purchase_id = ( isset( $row->id ) ? (int) $row->id : 0 );
            echo '<form method="post" action="' . esc_url( admin_url( 'admin-post.php' ) ) . '" style="margin:0;" onsubmit="return window.confirm(' . wp_json_encode( __( 'Are you sure you want to refund this payment? This cannot be undone.', 'bobasoftware-publisher-paywall' ) ) . ');">';
            echo '<input type="hidden" name="action" value="bspp_refund_purchase" />';
            echo '<input type="hidden" name="purchase_id" value="' . esc_attr( (string) $purchase_id ) . '" />';
            wp_nonce_field( 'bspp_refund_purchase' );
            echo '<button type="submit" class="button button-small">' . esc_html__( 'Refund', 'bobasoftware-publisher-paywall' ) . '</button>';
            echo '</form>';
        } elseif ( $show_freemius_refund_help ) {
            echo '<a class="button button-small" href="' . esc_url( $fs_dash ) . '" target="_blank" rel="noopener noreferrer">' . esc_html__( 'Refund in Freemius', 'bobasoftware-publisher-paywall' ) . '</a>';
        } elseif ( $show_sub_refund ) {
            $purchase_id = ( isset( $row->id ) ? (int) $row->id : 0 );
            $confirm1 = __( 'Are you sure you want to refund this payment? This cannot be undone.', 'bobasoftware-publisher-paywall' );
            $confirm2 = __( 'Giving a refund will also cancel the reader\'s subscription. Are you sure you want to proceed?', 'bobasoftware-publisher-paywall' );
            $cancel_field_id = 'bspp_also_cancel_sub_' . (string) $purchase_id;
            $onsubmit = 'if(!window.confirm(' . wp_json_encode( $confirm1 ) . ')){return false;}' . 'if(!window.confirm(' . wp_json_encode( $confirm2 ) . ')){return false;}' . 'var el=document.getElementById(' . wp_json_encode( $cancel_field_id ) . ');' . 'if(el){el.value=\'1\';}' . 'return true;';
            echo '<form method="post" action="' . esc_url( admin_url( 'admin-post.php' ) ) . '" style="margin:0;" onsubmit="' . esc_attr( $onsubmit ) . '">';
            echo '<input type="hidden" name="action" value="bspp_refund_purchase" />';
            echo '<input type="hidden" name="purchase_id" value="' . esc_attr( (string) $purchase_id ) . '" />';
            echo '<input type="hidden" id="' . esc_attr( $cancel_field_id ) . '" name="bspp_also_cancel_subscription" value="0" />';
            wp_nonce_field( 'bspp_refund_purchase' );
            echo '<button type="submit" class="button button-small">' . esc_html__( 'Refund', 'bobasoftware-publisher-paywall' ) . '</button>';
            echo '</form>';
        } else {
            echo '—';
        }
        echo '</td>';
        echo '</tr>';
    }

    /**
     * Resolve Payment Intent id and metadata post id from Stripe Checkout Session (Payments admin display only).
     *
     * @return array{payment_intent: string, post_id: int, invoice_id: string}
     */
    private function get_payments_row_checkout_session_meta( $stripe_session_id ) {
        $empty = [
            'payment_intent' => '',
            'post_id'        => 0,
            'invoice_id'     => '',
        ];
        $stripe_session_id = ( is_string( $stripe_session_id ) ? trim( $stripe_session_id ) : '' );
        if ( $stripe_session_id === '' || strpos( $stripe_session_id, 'cs_' ) !== 0 ) {
            return $empty;
        }
        $settings = new BSPP_Settings_Repository();
        $api_key = trim( (string) $settings->get( 'stripe_api_key', '' ) );
        if ( $api_key === '' ) {
            return $empty;
        }
        $cache_key = 'bobasoftware_pp_pay_cs_' . md5( $stripe_session_id . '|' . get_locale() );
        $cached = get_transient( $cache_key );
        if ( is_array( $cached ) && array_key_exists( 'payment_intent', $cached ) && array_key_exists( 'post_id', $cached ) && array_key_exists( 'invoice_id', $cached ) ) {
            return [
                'payment_intent' => ( is_string( $cached['payment_intent'] ) ? $cached['payment_intent'] : '' ),
                'post_id'        => ( isset( $cached['post_id'] ) ? (int) $cached['post_id'] : 0 ),
                'invoice_id'     => ( is_string( $cached['invoice_id'] ) ? $cached['invoice_id'] : '' ),
            ];
        }
        $checkout = new BSPP_Stripe_Checkout();
        $session = $checkout->retrieve_checkout_session( $stripe_session_id );
        if ( is_wp_error( $session ) ) {
            return $empty;
        }
        $pi = ( isset( $session['payment_intent'] ) ? $session['payment_intent'] : '' );
        if ( is_array( $pi ) ) {
            $pi = ( isset( $pi['id'] ) ? (string) $pi['id'] : '' );
        } else {
            $pi = ( is_string( $pi ) ? $pi : '' );
        }
        $meta = ( isset( $session['metadata'] ) && is_array( $session['metadata'] ) ? $session['metadata'] : [] );
        $post_id = ( isset( $meta['post_id'] ) ? (int) $meta['post_id'] : 0 );
        $inv_raw = ( isset( $session['invoice'] ) ? $session['invoice'] : '' );
        if ( is_array( $inv_raw ) ) {
            $inv_raw = ( isset( $inv_raw['id'] ) ? (string) $inv_raw['id'] : '' );
        } else {
            $inv_raw = ( is_string( $inv_raw ) ? $inv_raw : '' );
        }
        $invoice_id = ( $inv_raw !== '' && strpos( $inv_raw, 'in_' ) === 0 ? $inv_raw : '' );
        $payload = [
            'payment_intent' => $pi,
            'post_id'        => $post_id,
            'invoice_id'     => $invoice_id,
        ];
        set_transient( $cache_key, $payload, DAY_IN_SECONDS );
        return $payload;
    }

    public function render_tools_admin_page() {
        if ( !current_user_can( 'manage_options' ) ) {
            return;
        }
    }

    public function render_help_admin_page() {
        if ( !current_user_can( 'manage_options' ) ) {
            return;
        }
        $support_line = sprintf( 
            /* translators: %s: support email address */
            __( 'Need help? Email us at %s.', 'bobasoftware-publisher-paywall' ),
            '<a href="mailto:hi@bobasoftware.com" class="sp-help-contact__email">hi@bobasoftware.com</a>'
         );
        $step_titles = [
            __( 'Step 1 — Install the plugin', 'bobasoftware-publisher-paywall' ),
            __( 'Step 2 — Complete the setup wizard', 'bobasoftware-publisher-paywall' ),
            __( 'Step 3 — Connect Stripe', 'bobasoftware-publisher-paywall' ),
            __( 'Step 4 — Launch your paywall', 'bobasoftware-publisher-paywall' )
        ];
        $step_bodies = [
            __( 'Download Bobasoftware Publisher Paywall from WordPress.org and upload it to your WordPress site. Go to Plugins → Add New → Upload Plugin, select the .zip file, and activate it.', 'bobasoftware-publisher-paywall' ),
            __( 'After activation, Publisher Paywall will guide you through a short setup wizard. Choose your subscription options, set your free publish window, and connect Stripe.', 'bobasoftware-publisher-paywall' ),
            __( 'Enter your Stripe API key and webhook secret in the setup wizard or under Publisher Paywall → Settings. Publisher Paywall uses Stripe to process all reader payments securely.', 'bobasoftware-publisher-paywall' ),
            __( 'Click \'Launch paywall\' at the end of the wizard. Your posts will automatically become premium after the number of days you set. Readers will see the paywall when they try to access locked content.', 'bobasoftware-publisher-paywall' )
        ];
        $faq_list = [
            [
                'q' => __( 'How does the free publish window work?', 'bobasoftware-publisher-paywall' ),
                'a' => __( 'You set a number of days (for example, 7). Any post published within that window is free for everyone to read — no account needed. After that time passes, the post becomes premium and the paywall appears for readers who don\'t have access.', 'bobasoftware-publisher-paywall' ),
            ],
            [
                'q' => __( 'Can readers cancel their subscription themselves?', 'bobasoftware-publisher-paywall' ),
                'a' => __( 'Yes. Paid subscribers see a \'Manage subscription\' link at the end of each unlocked article. That opens a secure Stripe page where they can cancel, update their payment method, or view past invoices — they do not need to contact you. On themes that register a footer menu location, the same link can also be added to a footer menu.', 'bobasoftware-publisher-paywall' ),
            ],
            [
                'q' => __( 'What happens when a reader\'s subscription expires?', 'bobasoftware-publisher-paywall' ),
                'a' => __( 'They lose access to premium content and will see the paywall again. They can resubscribe at any time.', 'bobasoftware-publisher-paywall' ),
            ],
            [
                'q' => __( 'I can\'t see a reader in the Members page. Why?', 'bobasoftware-publisher-paywall' ),
                'a' => __( 'Readers only appear after they log in or make a payment. Anonymous readers who haven\'t created an account won\'t show up yet.', 'bobasoftware-publisher-paywall' ),
            ]
        ];
        echo '<div class="wrap sp-help-wrap">';
        echo '<h1 class="sp-help-title">' . esc_html__( 'Help', 'bobasoftware-publisher-paywall' ) . '</h1>';
        echo '<div class="sp-help-stack">';
        echo '<section class="sp-help-section" aria-labelledby="sp-help-getting-started-heading">';
        echo '<h2 id="sp-help-getting-started-heading" class="sp-help-section__title">' . esc_html__( 'Getting Started', 'bobasoftware-publisher-paywall' ) . '</h2>';
        echo '<p class="sp-help-intro">' . esc_html__( 'New to Publisher Paywall? Follow these steps to get set up.', 'bobasoftware-publisher-paywall' ) . '</p>';
        echo '<div class="sp-help-steps">';
        foreach ( $step_titles as $i => $title ) {
            echo '<div class="sp-help-card">';
            echo '<h3 class="sp-help-card__title">' . esc_html( $title ) . '</h3>';
            echo '<p class="sp-help-card__body">' . esc_html( $step_bodies[$i] ) . '</p>';
            echo '</div>';
        }
        echo '</div>';
        echo '<div class="sp-help-card">';
        echo '<h3 class="sp-help-card__title">' . esc_html__( 'Recommended themes', 'bobasoftware-publisher-paywall' ) . '</h3>';
        echo '<p class="sp-help-card__body">' . esc_html__( 'Publisher Paywall works best with themes that support post summaries on the homepage. We recommend Astra, GeneratePress, or Kadence — these themes work well with Publisher Paywall out of the box. The default Twenty Twenty-Five theme may show full post content on the homepage instead of summaries.', 'bobasoftware-publisher-paywall' ) . '</p>';
        echo '</div>';
        echo '</section>';
        $email_setup_intro = __( 'Magic login links are how readers get back into their account after paying. For these links to arrive, your WordPress site needs to be able to send email reliably — and most hosts don\'t support this out of the box.', 'bobasoftware-publisher-paywall' );
        $email_setup_step_titles = [__( 'Install a free SMTP plugin', 'bobasoftware-publisher-paywall' ), __( 'Connect a free email-sending provider', 'bobasoftware-publisher-paywall' ), __( 'Run a test send', 'bobasoftware-publisher-paywall' )];
        $email_setup_step_bodies = [__( 'From your WordPress dashboard, go to Plugins → Add New, and search for "WP Mail SMTP" (or "Easy WP SMTP"). Install and activate it.', 'bobasoftware-publisher-paywall' ), __( 'WP Mail SMTP will walk you through connecting a provider. Most publishers use a free tier from a service like Brevo or SendGrid — both offer enough free monthly email sends to cover magic login links for most sites. Follow the plugin\'s setup wizard and enter the credentials it asks for.', 'bobasoftware-publisher-paywall' ), __( 'Once connected, go to Publisher Paywall → Settings, find the Email Delivery section, and click "Send test email." If it succeeds, your status will show "Working" and you\'re done. If it fails, the error message shown can help you (or your provider\'s support) pinpoint what\'s wrong.', 'bobasoftware-publisher-paywall' )];
        $email_setup_still_stuck = sprintf( 
            /* translators: %s: support email address link */
            __( 'Still stuck? Contact us at %s and we\'ll help you sort it out.', 'bobasoftware-publisher-paywall' ),
            '<a href="mailto:hi@bobasoftware.com" class="sp-help-contact__email">hi@bobasoftware.com</a>'
         );
        echo '<section class="sp-help-section" aria-labelledby="sp-help-email-setup-heading">';
        echo '<h2 id="sp-help-email-setup-heading" class="sp-help-section__title">' . esc_html__( 'Setting up email for magic login', 'bobasoftware-publisher-paywall' ) . '</h2>';
        echo '<p class="sp-help-intro">' . esc_html( $email_setup_intro ) . '</p>';
        echo '<div class="sp-help-steps">';
        foreach ( $email_setup_step_titles as $i => $title ) {
            echo '<div class="sp-help-card">';
            echo '<h3 class="sp-help-card__title">' . esc_html( $i + 1 . '. ' . $title ) . '</h3>';
            echo '<p class="sp-help-card__body">' . esc_html( $email_setup_step_bodies[$i] ) . '</p>';
            echo '</div>';
        }
        echo '</div>';
        echo '<p class="sp-help-intro">';
        echo wp_kses( $email_setup_still_stuck, [
            'a' => [
                'href'  => [],
                'class' => [],
            ],
        ] );
        echo '</p>';
        echo '</section>';
        echo '<section class="sp-help-section" aria-labelledby="sp-help-faq-heading">';
        echo '<h2 id="sp-help-faq-heading" class="sp-help-section__title">' . esc_html__( 'Common Questions', 'bobasoftware-publisher-paywall' ) . '</h2>';
        echo '<div class="sp-help-faq">';
        foreach ( $faq_list as $faq ) {
            echo '<div class="sp-help-faq-item">';
            echo '<h3 class="sp-help-faq-item__question">' . esc_html( $faq['q'] ) . '</h3>';
            echo '<p class="sp-help-faq-item__answer">' . esc_html( $faq['a'] ) . '</p>';
            echo '</div>';
        }
        echo '</div>';
        $upgrade_url = admin_url( 'admin.php?page=bobasoftware-publisher-paywall-upgrade' );
        echo '<p class="sp-help-intro">';
        echo esc_html__( 'Looking for tip unlock, per-article purchases, or free-post quotas? These features are available in Publisher Paywall Pro.', 'bobasoftware-publisher-paywall' );
        echo ' ';
        echo '<a href="' . esc_url( $upgrade_url ) . '">' . esc_html__( 'Upgrade to Pro', 'bobasoftware-publisher-paywall' ) . '</a>';
        echo '</p>';
        echo '</section>';
        echo '<section class="sp-help-section" aria-labelledby="sp-help-contact-heading">';
        echo '<h2 id="sp-help-contact-heading" class="sp-help-section__title">' . esc_html__( 'Contact Support', 'bobasoftware-publisher-paywall' ) . '</h2>';
        echo '<div class="sp-help-contact-card"><p>';
        echo wp_kses( $support_line, [
            'a' => [
                'href'  => [],
                'class' => [],
            ],
        ] );
        echo '</p></div>';
        echo '</section>';
        echo '</div>';
        echo '</div>';
    }

    public function render_members_admin_page() {
        if ( !current_user_can( 'manage_options' ) ) {
            return;
        }
        $rows = $this->get_members_admin_rows();
        echo '<div class="wrap">';
        echo '<h1>' . esc_html__( 'Members', 'bobasoftware-publisher-paywall' ) . '</h1>';
        echo '<p class="description">' . esc_html__( 'Review membership status and unlocked posts for readers.', 'bobasoftware-publisher-paywall' ) . '</p>';
        // phpcs:disable WordPress.Security.NonceVerification.Recommended -- admin notice URL parameters, not form submissions.
        if ( isset( $_GET['bspp_members_cancel'] ) && sanitize_key( wp_unslash( $_GET['bspp_members_cancel'] ) ) === '1' ) {
            echo '<div class="notice notice-success is-dismissible"><p>' . esc_html__( 'Cancellation scheduled in Stripe. The reader keeps access until the end of the current billing period.', 'bobasoftware-publisher-paywall' ) . '</p></div>';
        }
        if ( isset( $_GET['bspp_members_cancel_error'] ) ) {
            $err_detail = ( isset( $_GET['bspp_members_cancel_msg'] ) ? sanitize_text_field( wp_unslash( $_GET['bspp_members_cancel_msg'] ) ) : '' );
            echo '<div class="notice notice-error is-dismissible"><p>' . esc_html__( 'Could not cancel membership in Stripe.', 'bobasoftware-publisher-paywall' );
            if ( $err_detail !== '' ) {
                echo ' ' . esc_html( $err_detail );
            }
            echo '</p></div>';
        }
        // phpcs:enable WordPress.Security.NonceVerification.Recommended
        if ( empty( $rows ) ) {
            echo '<p>' . esc_html__( 'No members or unlock activity found yet.', 'bobasoftware-publisher-paywall' ) . '</p>';
            echo '</div>';
            return;
        }
        echo '<table class="widefat striped">';
        echo '<thead><tr>';
        echo '<th>' . esc_html__( 'User', 'bobasoftware-publisher-paywall' ) . '</th>';
        echo '<th>' . esc_html__( 'Email', 'bobasoftware-publisher-paywall' ) . '</th>';
        echo '<th>' . esc_html__( 'Membership status', 'bobasoftware-publisher-paywall' ) . '</th>';
        echo '<th>' . esc_html__( 'Subscription state', 'bobasoftware-publisher-paywall' ) . '</th>';
        echo '<th>' . esc_html__( 'Unlocked free posts', 'bobasoftware-publisher-paywall' ) . '</th>';
        echo '<th>' . esc_html__( 'Cancel status', 'bobasoftware-publisher-paywall' ) . '</th>';
        echo '<th>' . esc_html__( 'Actions', 'bobasoftware-publisher-paywall' ) . '</th>';
        echo '</tr></thead><tbody>';
        foreach ( $rows as $row ) {
            $display_name = ( is_string( $row->display_name ?? '' ) && $row->display_name !== '' ? $row->display_name : '(no name)' );
            $email = ( is_string( $row->email ?? '' ) ? $row->email : '' );
            $raw_membership_status = sanitize_key( (string) ($row->membership_status ?? '') );
            $status = $this->normalize_membership_status( $row->membership_status ?? '' );
            $plan = sanitize_key( (string) ($row->membership_plan ?? '') );
            $period_end = ( is_string( $row->current_period_end ?? '' ) ? $row->current_period_end : '' );
            $unlock_count = ( isset( $row->unlock_count ) ? (int) $row->unlock_count : 0 );
            $membership_id = ( isset( $row->membership_id ) ? (int) $row->membership_id : 0 );
            $stripe_sub = ( isset( $row->stripe_subscription_id ) ? (string) $row->stripe_subscription_id : '' );
            $cancel_at_end = !empty( $row->cancel_at_period_end );
            $subscription_state = 'None';
            if ( $plan !== '' ) {
                $subscription_state = ucwords( str_replace( '_', ' ', $plan ) );
                if ( $period_end !== '' && $period_end !== '0000-00-00 00:00:00' ) {
                    $period_end_label = mysql2date( get_option( 'date_format' ), $period_end );
                    if ( is_string( $period_end_label ) && $period_end_label !== '' ) {
                        $subscription_state .= ' (ends ' . $period_end_label . ')';
                    }
                }
                if ( $status === 'Active' && strpos( $subscription_state, '(ends ' ) === false ) {
                    if ( $cancel_at_end ) {
                        $subscription_state .= ' (will not renew)';
                    } elseif ( $plan === 'monthly' ) {
                        $subscription_state .= ' (renews monthly)';
                    } elseif ( $plan === 'annual' ) {
                        $subscription_state .= ' (renews annually)';
                    }
                }
            }
            $cancel_status = __( 'No', 'bobasoftware-publisher-paywall' );
            if ( in_array( $raw_membership_status, ['canceled', 'cancelled'], true ) ) {
                $cancel_status = __( 'Ended', 'bobasoftware-publisher-paywall' );
            } elseif ( $cancel_at_end && $raw_membership_status === 'active' ) {
                $cancel_status = __( 'Scheduled (access until period end)', 'bobasoftware-publisher-paywall' );
            }
            $show_cancel = $raw_membership_status === 'active' && !$cancel_at_end && $membership_id > 0 && $stripe_sub !== '' && strpos( $stripe_sub, 'sub_' ) === 0;
            echo '<tr>';
            echo '<td>' . esc_html( $display_name ) . '</td>';
            echo '<td>' . esc_html( $email ) . '</td>';
            echo '<td>' . esc_html( $status ) . '</td>';
            echo '<td>' . esc_html( $subscription_state ) . '</td>';
            echo '<td>' . esc_html( (string) $unlock_count ) . '</td>';
            echo '<td>' . esc_html( $cancel_status ) . '</td>';
            echo '<td>';
            if ( $show_cancel ) {
                echo '<form method="post" action="' . esc_url( admin_url( 'admin-post.php' ) ) . '" style="margin:0;" onsubmit="return window.confirm(' . wp_json_encode( __( 'Are you sure you want to cancel this reader\'s membership? They will retain access until the end of their current billing period.', 'bobasoftware-publisher-paywall' ) ) . ');">';
                echo '<input type="hidden" name="action" value="bspp_cancel_membership" />';
                echo '<input type="hidden" name="membership_id" value="' . esc_attr( (string) $membership_id ) . '" />';
                wp_nonce_field( 'bspp_cancel_membership' );
                echo '<button type="submit" class="button button-small">' . esc_html__( 'Cancel membership', 'bobasoftware-publisher-paywall' ) . '</button>';
                echo '</form>';
            } else {
                echo '—';
            }
            echo '</td>';
            echo '</tr>';
        }
        echo '</tbody></table>';
        echo '</div>';
    }

    public function render_payments_admin_page() {
        if ( !current_user_can( 'manage_options' ) ) {
            return;
        }
        $per_page = 20;
        // phpcs:disable WordPress.Security.NonceVerification.Recommended -- payments admin URL state, not form submissions.
        $current_page = ( isset( $_GET['bspp_page_num'] ) ? max( 1, absint( wp_unslash( (string) $_GET['bspp_page_num'] ) ) ) : 1 );
        $offset = ($current_page - 1) * $per_page;
        $data = $this->get_payments_admin_rows( $offset, $per_page );
        $rows = $data['rows'];
        $total = (int) $data['total'];
        $total_pages = max( 1, (int) ceil( $total / $per_page ) );
        $pending_count = $this->get_payments_pending_count();
        $show_abandoned = isset( $_GET['bspp_show_abandoned'] ) && sanitize_key( wp_unslash( (string) $_GET['bspp_show_abandoned'] ) ) === '1';
        $base_url = admin_url( 'admin.php?page=bobasoftware-publisher-paywall-payments' );
        if ( $current_page > 1 ) {
            $base_url = add_query_arg( 'bspp_page_num', (string) $current_page, $base_url );
        }
        echo '<div class="wrap">';
        echo '<h1>' . esc_html__( 'Payments', 'bobasoftware-publisher-paywall' ) . '</h1>';
        echo '<p class="description">' . esc_html__( 'Simple ledger view of purchases and statuses.', 'bobasoftware-publisher-paywall' ) . '</p>';
        echo '<div class="bspp-payments-upsell-banner" role="note">';
        echo '<p>' . esc_html__( 'Want to earn from tips and per-article purchases too? Try Pro free for 14 days.', 'bobasoftware-publisher-paywall' ) . '</p>';
        echo '<a href="' . esc_url( bspp_fs()->get_trial_url() ) . '">' . esc_html__( 'Start free trial →', 'bobasoftware-publisher-paywall' ) . '</a>';
        echo '</div>';
        if ( isset( $_GET['bspp_refund'] ) && sanitize_key( wp_unslash( $_GET['bspp_refund'] ) ) === '1' ) {
            echo '<div class="notice notice-success is-dismissible"><p>' . esc_html__( 'Refund processed. The payment status has been updated.', 'bobasoftware-publisher-paywall' ) . '</p></div>';
            if ( isset( $_GET['bspp_refund_cancel_warn'] ) && sanitize_key( wp_unslash( $_GET['bspp_refund_cancel_warn'] ) ) === '1' ) {
                $wmsg = ( isset( $_GET['bspp_refund_cancel_msg'] ) ? sanitize_text_field( wp_unslash( $_GET['bspp_refund_cancel_msg'] ) ) : '' );
                echo '<div class="notice notice-warning is-dismissible"><p>' . esc_html__( 'Refund completed, but the subscription could not be scheduled for cancellation in Stripe.', 'bobasoftware-publisher-paywall' );
                if ( $wmsg !== '' ) {
                    echo ' ' . esc_html( $wmsg );
                }
                echo '</p></div>';
            }
        }
        if ( isset( $_GET['bspp_refund_error'] ) ) {
            $err_detail = ( isset( $_GET['bspp_refund_msg'] ) ? sanitize_text_field( wp_unslash( $_GET['bspp_refund_msg'] ) ) : '' );
            echo '<div class="notice notice-error is-dismissible"><p>' . esc_html__( 'Could not refund this payment.', 'bobasoftware-publisher-paywall' );
            if ( $err_detail !== '' ) {
                echo ' ' . esc_html( $err_detail );
            }
            echo '</p></div>';
        }
        if ( isset( $_GET['bspp_refund_freemius'] ) && sanitize_key( wp_unslash( (string) $_GET['bspp_refund_freemius'] ) ) === '1' ) {
            $fsu = ( isset( $_GET['bspp_refund_fs_url'] ) ? esc_url_raw( wp_unslash( (string) $_GET['bspp_refund_fs_url'] ) ) : '' );
            if ( $fsu === '' || !wp_http_validate_url( $fsu ) ) {
                $fsu = 'https://dashboard.freemius.com/';
            }
            echo '<div class="notice notice-info is-dismissible"><p>';
            echo esc_html__( 'This payment was processed through Freemius. Open your Freemius seller dashboard to issue the refund.', 'bobasoftware-publisher-paywall' );
            echo ' ';
            echo '<a href="' . esc_url( $fsu ) . '" target="_blank" rel="noopener noreferrer">' . esc_html__( 'Open Freemius dashboard', 'bobasoftware-publisher-paywall' ) . '</a>';
            echo '</p></div>';
        }
        // phpcs:enable WordPress.Security.NonceVerification.Recommended
        if ( empty( $rows ) && $pending_count === 0 ) {
            echo '<p>' . esc_html__( 'No payments found yet.', 'bobasoftware-publisher-paywall' ) . '</p>';
            echo '</div>';
            return;
        }
        if ( $pending_count > 0 ) {
            echo '<p class="sp-payments-abandoned-toggle">';
            if ( $show_abandoned ) {
                $toggle_url = remove_query_arg( 'bspp_show_abandoned', $base_url );
                echo '<a href="' . esc_url( $toggle_url ) . '">' . esc_html__( 'Hide abandoned checkouts', 'bobasoftware-publisher-paywall' ) . '</a>';
            } else {
                $toggle_url = add_query_arg( 'bspp_show_abandoned', '1', $base_url );
                echo '<a href="' . esc_url( $toggle_url ) . '">' . esc_html( sprintf( 
                    /* translators: %d: number of abandoned checkout sessions */
                    __( 'Show abandoned checkouts (%d)', 'bobasoftware-publisher-paywall' ),
                    $pending_count
                 ) ) . '</a>';
            }
            echo '</p>';
        }
        if ( empty( $rows ) ) {
            echo '<p>' . esc_html__( 'No completed payments yet.', 'bobasoftware-publisher-paywall' ) . '</p>';
        } else {
            $this->render_payments_admin_table( $rows, false );
            if ( $total_pages > 1 ) {
                $pagination_base = admin_url( 'admin.php?page=bobasoftware-publisher-paywall-payments' );
                if ( $show_abandoned ) {
                    $pagination_base = add_query_arg( 'bspp_show_abandoned', '1', $pagination_base );
                }
                echo '<div class="tablenav"><div class="tablenav-pages" style="margin:12px 0 0;">';
                if ( $current_page > 1 ) {
                    $prev_url = add_query_arg( 'bspp_page_num', (string) ($current_page - 1), $pagination_base );
                    echo '<a class="button" href="' . esc_url( $prev_url ) . '">Previous</a> ';
                }
                echo '<span class="displaying-num">Page ' . esc_html( (string) $current_page ) . ' of ' . esc_html( (string) $total_pages ) . '</span>';
                if ( $current_page < $total_pages ) {
                    $next_url = add_query_arg( 'bspp_page_num', (string) ($current_page + 1), $pagination_base );
                    echo ' <a class="button" href="' . esc_url( $next_url ) . '">Next</a>';
                }
                echo '</div></div>';
            }
        }
        if ( $show_abandoned && $pending_count > 0 ) {
            $pending_rows = $this->get_payments_admin_pending_rows();
            echo '<div class="sp-payments-abandoned-section">';
            echo '<h2>' . esc_html__( 'Abandoned checkouts', 'bobasoftware-publisher-paywall' ) . '</h2>';
            echo '<p class="description">' . esc_html__( 'Stripe Checkout sessions that were started but not completed. These are not real payments.', 'bobasoftware-publisher-paywall' ) . '</p>';
            if ( empty( $pending_rows ) ) {
                echo '<p>' . esc_html__( 'No abandoned checkouts.', 'bobasoftware-publisher-paywall' ) . '</p>';
            } else {
                $this->render_payments_admin_table( $pending_rows, true );
            }
            echo '</div>';
        }
        echo '</div>';
    }

    /**
     * Reader-facing Stripe Customer Billing Portal (requires Customer Portal enabled in Stripe Dashboard).
     */
    public function handle_stripe_customer_portal() {
        if ( !is_user_logged_in() ) {
            wp_safe_redirect( home_url( '/' ) );
            exit;
        }
        if ( !isset( $_REQUEST['_wpnonce'] ) || !wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['_wpnonce'] ) ), 'bspp_customer_portal' ) ) {
            wp_die( esc_html__( 'Invalid request.', 'bobasoftware-publisher-paywall' ), esc_html__( 'Error', 'bobasoftware-publisher-paywall' ), [
                'response' => 403,
            ] );
        }
        $membership_repo = new BSPP_Membership_Repository();
        $row = $membership_repo->get_active( get_current_user_id() );
        $customer_id = ( $row && !empty( $row->stripe_customer_id ) ? (string) $row->stripe_customer_id : '' );
        if ( $customer_id === '' || strpos( $customer_id, 'cus_' ) !== 0 ) {
            wp_safe_redirect( home_url( '/' ) );
            exit;
        }
        $return_url = home_url( '/' );
        $checkout = new BSPP_Stripe_Checkout();
        $session = $checkout->create_billing_portal_session( $customer_id, $return_url );
        if ( is_wp_error( $session ) || empty( $session['url'] ) ) {
            wp_safe_redirect( home_url( '/?bspp_portal=error' ) );
            exit;
        }
        wp_safe_redirect( esc_url_raw( $session['url'] ) );
        exit;
    }

    /**
     * Publisher: schedule Stripe subscription cancel at period end for a member row.
     */
    public function handle_cancel_membership() {
        if ( !current_user_can( 'manage_options' ) ) {
            wp_die( esc_html__( 'You do not have permission to perform this action.', 'bobasoftware-publisher-paywall' ) );
        }
        check_admin_referer( 'bspp_cancel_membership' );
        $membership_id = ( isset( $_POST['membership_id'] ) ? (int) $_POST['membership_id'] : 0 );
        $base = admin_url( 'admin.php?page=bobasoftware-publisher-paywall-members' );
        $fail = add_query_arg( 'bspp_members_cancel_error', '1', $base );
        if ( $membership_id <= 0 ) {
            wp_safe_redirect( $fail );
            exit;
        }
        $membership_repo = new BSPP_Membership_Repository();
        $row = $membership_repo->get_by_id( $membership_id );
        if ( !$row || sanitize_key( (string) $row->status ) !== 'active' ) {
            wp_safe_redirect( $fail );
            exit;
        }
        if ( !empty( $row->cancel_at_period_end ) ) {
            wp_safe_redirect( $fail );
            exit;
        }
        $stripe_sub = ( isset( $row->stripe_subscription_id ) ? (string) $row->stripe_subscription_id : '' );
        if ( $stripe_sub === '' || strpos( $stripe_sub, 'sub_' ) !== 0 ) {
            wp_safe_redirect( $fail );
            exit;
        }
        $checkout = new BSPP_Stripe_Checkout();
        $data = $checkout->cancel_subscription_at_period_end( $stripe_sub );
        if ( is_wp_error( $data ) ) {
            $fail_msg = add_query_arg( [
                'bspp_members_cancel_error' => '1',
                'bspp_members_cancel_msg'   => rawurlencode( wp_strip_all_tags( $data->get_error_message() ) ),
            ], $base );
            wp_safe_redirect( $fail_msg );
            exit;
        }
        $period_end = null;
        if ( !empty( $data['current_period_end'] ) && is_numeric( $data['current_period_end'] ) ) {
            $period_end = gmdate( 'Y-m-d H:i:s', (int) $data['current_period_end'] );
        }
        $membership_repo->update_status_and_period(
            $membership_id,
            'active',
            $period_end,
            1
        );
        wp_safe_redirect( add_query_arg( 'bspp_members_cancel', '1', $base ) );
        exit;
    }

    /**
     * Resolve WP user id from a purchase row (metadata user_id, then purchase email).
     */
    private function resolve_purchase_user_id( $row ) {
        if ( !is_object( $row ) ) {
            return 0;
        }
        $user_id = ( isset( $row->user_id ) ? (int) $row->user_id : 0 );
        if ( $user_id > 0 ) {
            return $user_id;
        }
        $email = ( isset( $row->email ) ? sanitize_email( (string) $row->email ) : '' );
        if ( $email === '' ) {
            return 0;
        }
        $user = get_user_by( 'email', $email );
        return ( $user && !empty( $user->ID ) ? (int) $user->ID : 0 );
    }

    /**
     * Find the membership row to revoke after a subscription refund.
     */
    private function find_membership_for_subscription_refund( $user_id, $stripe_session_id = '' ) {
        $membership_repo = new BSPP_Membership_Repository();
        if ( $user_id > 0 ) {
            $active = $membership_repo->get_active( $user_id );
            if ( $active && !empty( $active->id ) ) {
                return $active;
            }
        }
        $stripe_session_id = ( is_string( $stripe_session_id ) ? trim( $stripe_session_id ) : '' );
        if ( $stripe_session_id === '' || strpos( $stripe_session_id, 'cs_' ) !== 0 ) {
            return null;
        }
        $checkout = new BSPP_Stripe_Checkout();
        $session = $checkout->retrieve_checkout_session( $stripe_session_id );
        if ( is_wp_error( $session ) ) {
            return null;
        }
        $sub_id = '';
        if ( !empty( $session['subscription'] ) ) {
            $sub_id = ( is_array( $session['subscription'] ) ? (string) ($session['subscription']['id'] ?? '') : (string) $session['subscription'] );
        }
        if ( $sub_id === '' || strpos( $sub_id, 'sub_' ) !== 0 ) {
            return null;
        }
        return $membership_repo->get_by_stripe_subscription_id( $sub_id );
    }

    /**
     * Publisher: refund per-article / tip (PaymentIntent) or subscription (Invoice); optional cancel subscription at period end.
     */
    public function handle_refund_purchase() {
        if ( !current_user_can( 'manage_options' ) ) {
            wp_die( esc_html__( 'You do not have permission to perform this action.', 'bobasoftware-publisher-paywall' ) );
        }
        check_admin_referer( 'bspp_refund_purchase' );
        $purchase_id = ( isset( $_POST['purchase_id'] ) ? (int) $_POST['purchase_id'] : 0 );
        $base = admin_url( 'admin.php?page=bobasoftware-publisher-paywall-payments' );
        $fail = add_query_arg( 'bspp_refund_error', '1', $base );
        if ( $purchase_id <= 0 ) {
            wp_safe_redirect( $fail );
            exit;
        }
        $purchase_repo = new BSPP_Purchase_Repository();
        $row = $purchase_repo->get_by_id( $purchase_id );
        if ( !$row ) {
            wp_safe_redirect( $fail );
            exit;
        }
        $type = sanitize_key( (string) $row->type );
        $raw_status = sanitize_key( (string) $row->status );
        if ( $raw_status === 'refunded' ) {
            wp_safe_redirect( $fail );
            exit;
        }
        if ( $raw_status !== 'succeeded' ) {
            wp_safe_redirect( $fail );
            exit;
        }
        $stripe_session_id = ( isset( $row->stripe_session_id ) ? (string) $row->stripe_session_id : '' );
        $session_meta = $this->get_payments_row_checkout_session_meta( $stripe_session_id );
        $use_freemius_refund = (bool) apply_filters(
            'bspp_purchase_use_freemius_refund',
            false,
            $row,
            $session_meta
        );
        if ( $use_freemius_refund ) {
            $dash_url = apply_filters(
                'bspp_freemius_refund_dashboard_url',
                'https://dashboard.freemius.com/',
                $row,
                $session_meta
            );
            if ( !is_string( $dash_url ) || $dash_url === '' ) {
                $dash_url = 'https://dashboard.freemius.com/';
            }
            wp_safe_redirect( add_query_arg( [
                'bspp_refund_freemius' => '1',
                'bspp_refund_fs_url'   => rawurlencode( $dash_url ),
            ], $base ) );
            exit;
        }
        $checkout = new BSPP_Stripe_Checkout();
        if ( in_array( $type, ['article_unlock', 'tip_unlock'], true ) ) {
            $pi = ( isset( $row->stripe_payment_intent_id ) ? trim( (string) $row->stripe_payment_intent_id ) : '' );
            if ( $pi === '' || strpos( $pi, 'pi_' ) !== 0 ) {
                $pi = ( isset( $session_meta['payment_intent'] ) ? (string) $session_meta['payment_intent'] : '' );
            }
            if ( $pi === '' || strpos( $pi, 'pi_' ) !== 0 ) {
                wp_safe_redirect( $fail );
                exit;
            }
            $idempotency_key = 'bspp_refund_purchase_' . (string) $purchase_id;
            $refund = $checkout->create_refund_for_payment_intent( $pi, $idempotency_key );
            if ( is_wp_error( $refund ) ) {
                $fail_msg = add_query_arg( [
                    'bspp_refund_error' => '1',
                    'bspp_refund_msg'   => rawurlencode( wp_strip_all_tags( $refund->get_error_message() ) ),
                ], $base );
                wp_safe_redirect( $fail_msg );
                exit;
            }
            $purchase_repo->update_status_by_id( $purchase_id, 'refunded' );
            $user_id = ( isset( $row->user_id ) ? (int) $row->user_id : 0 );
            $post_id = ( isset( $session_meta['post_id'] ) ? (int) $session_meta['post_id'] : 0 );
            if ( $stripe_session_id !== '' && strpos( $stripe_session_id, 'cs_' ) === 0 ) {
                delete_transient( 'bobasoftware_pp_pay_cs_' . md5( $stripe_session_id . '|' . get_locale() ) );
            }
            wp_safe_redirect( add_query_arg( 'bspp_refund', '1', $base ) );
            exit;
        }
        if ( in_array( $type, ['subscription_monthly', 'subscription_annual', 'subscription'], true ) ) {
            $invoice_id = ( isset( $row->stripe_invoice_id ) ? trim( (string) $row->stripe_invoice_id ) : '' );
            if ( $invoice_id === '' || strpos( $invoice_id, 'in_' ) !== 0 ) {
                $invoice_id = ( isset( $session_meta['invoice_id'] ) ? trim( (string) $session_meta['invoice_id'] ) : '' );
            }
            if ( $invoice_id === '' || strpos( $invoice_id, 'in_' ) !== 0 ) {
                wp_safe_redirect( $fail );
                exit;
            }
            $idempotency_key = 'bspp_refund_sub_purchase_' . (string) $purchase_id;
            $refund = $checkout->create_refund_for_invoice( $invoice_id, $idempotency_key );
            if ( is_wp_error( $refund ) ) {
                $fail_msg = add_query_arg( [
                    'bspp_refund_error' => '1',
                    'bspp_refund_msg'   => rawurlencode( wp_strip_all_tags( $refund->get_error_message() ) ),
                ], $base );
                wp_safe_redirect( $fail_msg );
                exit;
            }
            $purchase_repo->update_status_by_id( $purchase_id, 'refunded' );
            $user_id = $this->resolve_purchase_user_id( $row );
            $stripe_session_id = ( isset( $row->stripe_session_id ) ? (string) $row->stripe_session_id : '' );
            $membership_repo = new BSPP_Membership_Repository();
            $mem = $this->find_membership_for_subscription_refund( $user_id, $stripe_session_id );
            $stripe_sub = ( $mem && !empty( $mem->stripe_subscription_id ) ? (string) $mem->stripe_subscription_id : '' );
            $membership_revoked = false;
            if ( $mem && !empty( $mem->id ) ) {
                // Refunded subscription payments revoke access immediately (unlike cancel-at-period-end alone).
                $membership_revoked = (bool) $membership_repo->update_status_and_period(
                    (int) $mem->id,
                    'canceled',
                    null,
                    0
                );
            }
            $redirect_ok = add_query_arg( 'bspp_refund', '1', $base );
            $also_cancel = isset( $_POST['bspp_also_cancel_subscription'] ) && sanitize_text_field( wp_unslash( (string) $_POST['bspp_also_cancel_subscription'] ) ) === '1';
            if ( $also_cancel ) {
                if ( $stripe_sub !== '' && strpos( $stripe_sub, 'sub_' ) === 0 ) {
                    $cancel_res = $checkout->cancel_subscription_at_period_end( $stripe_sub );
                    if ( is_wp_error( $cancel_res ) ) {
                        $redirect_ok = add_query_arg( [
                            'bspp_refund'             => '1',
                            'bspp_refund_cancel_warn' => '1',
                            'bspp_refund_cancel_msg'  => rawurlencode( wp_strip_all_tags( $cancel_res->get_error_message() ) ),
                        ], $base );
                    }
                }
            }
            if ( $stripe_session_id !== '' && strpos( $stripe_session_id, 'cs_' ) === 0 ) {
                delete_transient( 'bobasoftware_pp_pay_cs_' . md5( $stripe_session_id . '|' . get_locale() ) );
            }
            wp_safe_redirect( $redirect_ok );
            exit;
        }
        wp_safe_redirect( $fail );
        exit;
    }

    /** @var int Nesting depth while rendering a footer template part (block themes). */
    private static $footer_template_part_depth = 0;

    /** @var bool Whether the manage-subscription link was added in the current footer template part. */
    private static $manage_subscription_footer_link_added = false;

    /** @var array<int, string> Per-request portal URL cache, keyed by user id. */
    private static $manage_subscription_portal_url_by_user = [];

    /**
     * Stripe Customer Portal URL for a user with an active paid membership, or empty string.
     */
    public static function get_manage_subscription_portal_url_for_user( $user_id = 0 ) {
        $user_id = ( $user_id > 0 ? (int) $user_id : get_current_user_id() );
        if ( $user_id <= 0 ) {
            return '';
        }
        if ( array_key_exists( $user_id, self::$manage_subscription_portal_url_by_user ) ) {
            return self::$manage_subscription_portal_url_by_user[$user_id];
        }
        $membership_repo = new BSPP_Membership_Repository();
        $row = $membership_repo->get_active( $user_id );
        $customer_id = ( $row && !empty( $row->stripe_customer_id ) ? (string) $row->stripe_customer_id : '' );
        if ( $customer_id === '' || strpos( $customer_id, 'cus_' ) !== 0 ) {
            self::$manage_subscription_portal_url_by_user[$user_id] = '';
            return '';
        }
        $portal_url = wp_nonce_url( admin_url( 'admin-post.php?action=bspp_stripe_customer_portal' ), 'bspp_customer_portal', '_wpnonce' );
        self::$manage_subscription_portal_url_by_user[$user_id] = $portal_url;
        return $portal_url;
    }

    private function should_skip_manage_subscription_footer_link() {
        if ( is_admin() || !is_user_logged_in() ) {
            return true;
        }
        return class_exists( 'BSPP_Magic_Login' ) && BSPP_Magic_Login::shortcode_used_this_request();
    }

    private function is_footer_nav_menu_location( $args ) {
        if ( empty( $args->theme_location ) ) {
            return false;
        }
        $location = (string) $args->theme_location;
        return $location === 'footer' || str_contains( $location, 'footer' );
    }

    /**
     * Classic themes: append to registered footer menu locations.
     */
    public function append_manage_subscription_footer_menu_item( $items, $args ) {
        if ( !$this->is_footer_nav_menu_location( $args ) || $this->should_skip_manage_subscription_footer_link() ) {
            return $items;
        }
        $portal_url = self::get_manage_subscription_portal_url_for_user();
        if ( $portal_url === '' ) {
            return $items;
        }
        $items .= '<li class="menu-item menu-item-type-custom menu-item-bspp-manage-subscription"><a href="' . esc_url( $portal_url ) . '">' . esc_html__( 'Manage subscription', 'bobasoftware-publisher-paywall' ) . '</a></li>';
        return $items;
    }

    public function track_footer_template_part_pre_render( $pre_render, $block ) {
        if ( is_admin() || wp_is_json_request() ) {
            return $pre_render;
        }
        if ( ($block['blockName'] ?? '') !== 'core/template-part' ) {
            return $pre_render;
        }
        $slug = ( isset( $block['attrs']['slug'] ) ? (string) $block['attrs']['slug'] : '' );
        if ( $slug === '' || !str_contains( $slug, 'footer' ) ) {
            return $pre_render;
        }
        self::$footer_template_part_depth++;
        if ( self::$footer_template_part_depth === 1 ) {
            self::$manage_subscription_footer_link_added = false;
        }
        return $pre_render;
    }

    public function track_footer_template_part_render_end( $block_content, $block ) {
        if ( ($block['blockName'] ?? '') !== 'core/template-part' ) {
            return $block_content;
        }
        $slug = ( isset( $block['attrs']['slug'] ) ? (string) $block['attrs']['slug'] : '' );
        if ( $slug === '' || !str_contains( $slug, 'footer' ) ) {
            return $block_content;
        }
        self::$footer_template_part_depth = max( 0, self::$footer_template_part_depth - 1 );
        return $block_content;
    }

    /**
     * Block themes: append to the first footer navigation block (inside the footer template part).
     */
    public function maybe_append_manage_subscription_block_navigation( $block_content, $block ) {
        if ( self::$footer_template_part_depth < 1 || self::$manage_subscription_footer_link_added || ($block['blockName'] ?? '') !== 'core/navigation' || $this->should_skip_manage_subscription_footer_link() ) {
            return $block_content;
        }
        $portal_url = self::get_manage_subscription_portal_url_for_user();
        if ( $portal_url === '' ) {
            return $block_content;
        }
        $link = '<li class="wp-block-navigation-item bspp-manage-subscription"><a class="wp-block-navigation-item__content" href="' . esc_url( $portal_url ) . '">' . esc_html__( 'Manage subscription', 'bobasoftware-publisher-paywall' ) . '</a></li>';
        if ( preg_match( '#</ul>#i', $block_content ) ) {
            $block_content = preg_replace(
                '#</ul>#i',
                $link . '</ul>',
                $block_content,
                1
            );
        } else {
            $block_content .= $link;
        }
        self::$manage_subscription_footer_link_added = true;
        return $block_content;
    }

}
