<?php
/**
 * Standalone customer area: subscriptions list, single view, account dashboard, profile, billing.
 *
 * @package Invizo\SubscriptionsLite\Frontend
 */

namespace Invizo\SubscriptionsLite\Frontend;

use Exception;
use Invizo\SubscriptionsLite\Gateway\StripeGateway;
use WP_Error;
use WP_Query;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

class Account {

    const QUERY_VAR = 'invizo_subscription';
    const TAB_QUERY = 'invizo_tab';

    public static function init() {
        add_shortcode( 'invizo_subscriptions', array( __CLASS__, 'render_portal' ) );
        add_shortcode( 'invizo_account', array( __CLASS__, 'render_account_dashboard' ) );
        add_action( 'wp_enqueue_scripts', array( __CLASS__, 'maybe_enqueue_assets' ) );
        add_action( 'init', array( __CLASS__, 'maybe_handle_subscription_actions' ), 21 );
        add_action( 'template_redirect', array( __CLASS__, 'maybe_redirect_stripe_portal' ), 5 );
    }

    public static function maybe_enqueue_assets() {
        global $post;
        if ( ! is_a( $post, 'WP_Post' ) ) {
            return;
        }
        $content = $post->post_content;
        if (
            has_shortcode( $content, 'invizo_subscriptions' )
            || has_shortcode( $content, 'invizo_account' )
            || has_shortcode( $content, 'invizo_auth' )
        ) {
            wp_enqueue_style(
                'invizo-account-portal',
                INVIZO_LITE_URL . 'css/account-portal.css',
                array(),
                INVIZO_LITE_VERSION
            );
        }
    }

    /**
     * Base URL for this shortcode’s links (current page).
     *
     * @return string
     */
    protected static function get_current_portal_base_url() {
        $url = get_permalink();
        return $url ? $url : home_url( '/' );
    }

    /**
     * Whether the logged-in user may view this subscription record.
     *
     * @param int $post_id invizo_sub post ID.
     * @return bool
     */
    public static function current_user_can_view_subscription( $post_id ) {
        $post_id = (int) $post_id;
        if ( $post_id <= 0 || ! is_user_logged_in() ) {
            return false;
        }

        $post = get_post( $post_id );
        if ( ! $post || $post->post_type !== 'invizo_sub' ) {
            return false;
        }

        $owner_id = (int) get_post_meta( $post_id, '_wp_user_id', true );
        if ( $owner_id && $owner_id === get_current_user_id() ) {
            return true;
        }

        $sub_email = get_post_meta( $post_id, '_customer_email', true );
        $user      = wp_get_current_user();
        if ( $sub_email && $user->user_email && strtolower( $sub_email ) === strtolower( $user->user_email ) ) {
            return true;
        }

        return (bool) apply_filters( 'invizo_user_can_view_subscription', false, $post_id, get_current_user_id() );
    }

    /**
     * @param int $user_id WordPress user ID.
     * @return int[] Subscription post IDs.
     */
    public static function get_subscription_ids_for_user( $user_id ) {
        $user_id = (int) $user_id;
        if ( $user_id <= 0 ) {
            return array();
        }

        $user  = get_userdata( $user_id );
        $email = $user ? $user->user_email : '';

        $meta_query = array( 'relation' => 'OR' );
        $meta_query[] = array(
            'key'   => '_wp_user_id',
            'value' => $user_id,
        );
        if ( $email ) {
            $meta_query[] = array(
                'key'   => '_customer_email',
                'value' => $email,
            );
        }

        $q = new WP_Query(
            array(
                'post_type'      => 'invizo_sub',
                'post_status'    => 'any',
                'posts_per_page' => 100,
                'fields'         => 'ids',
                'orderby'        => 'date',
                'order'          => 'DESC',
                'meta_query'     => $meta_query,
            )
        );

        return array_map( 'intval', $q->posts );
    }

    /**
     * @param int $plan_id Plan post ID.
     * @return string Human-readable amount / interval.
     */
    public static function format_plan_amount( $plan_id ) {
        $plan_id = (int) $plan_id;
        if ( ! $plan_id ) {
            return '—';
        }

        $plan_price = get_post_meta( $plan_id, '_price', true );
        $plan_interval_count  = get_post_meta( $plan_id, '_invizo_sub_interval_count', true ) ?: '1';
        $plan_interval_period = get_post_meta( $plan_id, '_invizo_sub_interval_period', true ) ?: 'Month';

        if ( ! get_post_meta( $plan_id, '_invizo_sub_interval_period', true ) && get_post_meta( $plan_id, '_interval', true ) ) {
            $plan_interval_period = get_post_meta( $plan_id, '_interval', true );
        }

        return '$' . number_format( (float) $plan_price, 2 ) . ' / ' . $plan_interval_count . ' ' . ucfirst( $plan_interval_period );
    }

    /**
     * Guest notice with login link.
     *
     * @return string
     */
    protected static function render_guest_notice() {
        $account = class_exists( Auth::class, false ) ? Auth::get_account_page_url() : home_url( '/' );
        $login   = class_exists( Auth::class, false ) ? Auth::get_login_page_url() : wp_login_url( $account );
        $register = class_exists( Auth::class, false ) ? Auth::get_register_page_url() : $login;
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only flag after checkout redirect.
        $after_subscribe = isset( $_GET['invizo_subscribed'] ) && (string) sanitize_text_field( wp_unslash( $_GET['invizo_subscribed'] ) ) === '1';

        ob_start();
        ?>
        <div class="invizo-portal invizo-portal--notice">
            <?php if ( $after_subscribe ) : ?>
                <div class="invizo-dashboard__notice invizo-dashboard__notice--success invizo-portal__post-checkout" role="status">
                    <?php esc_html_e( 'Your subscription is active.', 'invizo-subscriptions-lite' ); ?>
                </div>
                <p><?php esc_html_e( 'You are not logged in yet. Sign in with a site account that uses the same email address you paid with. If you need a new site account and registration is enabled, use the standard registration screen with that same email so Invizo can link your subscription.', 'invizo-subscriptions-lite' ); ?></p>
                <p>
                    <a class="invizo-portal__link" href="<?php echo esc_url( $login ); ?>"><?php esc_html_e( 'Log in', 'invizo-subscriptions-lite' ); ?></a>
                    <?php if ( get_option( 'users_can_register' ) && $register !== $login ) : ?>
                        <span class="invizo-portal__post-checkout-sep" aria-hidden="true"> · </span>
                        <a class="invizo-portal__link" href="<?php echo esc_url( $register ); ?>"><?php esc_html_e( 'Create an account', 'invizo-subscriptions-lite' ); ?></a>
                    <?php endif; ?>
                </p>
            <?php else : ?>
                <p><?php esc_html_e( 'Please log in to view your subscriptions.', 'invizo-subscriptions-lite' ); ?></p>
                <p><a class="invizo-portal__link" href="<?php echo esc_url( $login ); ?>"><?php esc_html_e( 'Log in or register', 'invizo-subscriptions-lite' ); ?></a></p>
            <?php endif; ?>
        </div>
        <?php
        return ob_get_clean();
    }

    /**
     * Shortcode: list + optional single view via ?invizo_subscription=ID
     *
     * @param array $atts Shortcode attributes (unused).
     * @return string
     */
    public static function render_portal( $atts ) {
        if ( ! is_user_logged_in() ) {
            return self::render_guest_notice();
        }

        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only subscription ID for portal view.
        $single_id = isset( $_GET[ self::QUERY_VAR ] ) ? absint( wp_unslash( $_GET[ self::QUERY_VAR ] ) ) : 0;
        $base      = self::get_current_portal_base_url();

        if ( $single_id > 0 ) {
            return self::render_single_subscription( $single_id, $base );
        }

        return self::render_subscription_list( $base );
    }

    /**
     * Shortcode: WooCommerce-style dashboard (nav + subscriptions / profile / billing).
     *
     * @param array $atts Shortcode attributes.
     * @return string
     */
    public static function render_account_dashboard( $atts ) {
        if ( ! is_user_logged_in() ) {
            return self::render_guest_notice();
        }

        $base = self::get_current_portal_base_url();
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only subscription ID for dashboard single view.
        $single_id = isset( $_GET[ self::QUERY_VAR ] ) ? absint( wp_unslash( $_GET[ self::QUERY_VAR ] ) ) : 0;

        if ( $single_id > 0 ) {
            $inner = self::render_single_subscription( $single_id, $base, true );
            return self::wrap_dashboard( $inner, 'subscriptions', $base );
        }

        // phpcs:disable WordPress.Security.NonceVerification.Recommended -- read-only GET for active tab and redirect flash notices.
        $tab = isset( $_GET[ self::TAB_QUERY ] ) ? sanitize_key( wp_unslash( $_GET[ self::TAB_QUERY ] ) ) : 'subscriptions';
        if ( ! in_array( $tab, array( 'subscriptions', 'profile', 'billing' ), true ) ) {
            $tab = 'subscriptions';
        }

        $notice = '';
        if ( isset( $_GET['invizo_billing_err'] ) ) {
            $msg     = sanitize_text_field( wp_unslash( $_GET['invizo_billing_err'] ) );
            $notice .= '<div class="invizo-dashboard__notice invizo-dashboard__notice--error" role="alert">' . esc_html( $msg ) . '</div>';
        }
        if ( isset( $_GET['invizo_pending_verify'] ) && (string) sanitize_text_field( wp_unslash( $_GET['invizo_pending_verify'] ) ) === '1' ) {
            $notice .= '<div class="invizo-dashboard__notice invizo-dashboard__notice--success" role="status">' . esc_html__( 'Payment succeeded. Check your email and click the confirmation link to activate your subscription in this account.', 'invizo-subscriptions-lite' ) . '</div>';
        }
        if ( isset( $_GET['invizo_email_verified'] ) && (string) sanitize_text_field( wp_unslash( $_GET['invizo_email_verified'] ) ) === '1' ) {
            $notice .= '<div class="invizo-dashboard__notice invizo-dashboard__notice--success" role="status">' . esc_html__( 'Your email is confirmed. Your subscription is now active in your account.', 'invizo-subscriptions-lite' ) . '</div>';
        }
        if ( isset( $_GET['invizo_verify'] ) ) {
            $ver = sanitize_key( wp_unslash( $_GET['invizo_verify'] ) );
            if ( $ver === 'expired' ) {
                $notice .= '<div class="invizo-dashboard__notice invizo-dashboard__notice--error" role="alert">' . esc_html__( 'That confirmation link has expired. Please contact support or complete checkout again.', 'invizo-subscriptions-lite' ) . '</div>';
            } elseif ( $ver === 'invalid' ) {
                $notice .= '<div class="invizo-dashboard__notice invizo-dashboard__notice--error" role="alert">' . esc_html__( 'That confirmation link is invalid. Open the latest email we sent or contact support.', 'invizo-subscriptions-lite' ) . '</div>';
            }
        }
        if ( isset( $_GET['invizo_cancel_ok'] ) && (string) sanitize_text_field( wp_unslash( $_GET['invizo_cancel_ok'] ) ) === '1' ) {
            $notice .= '<div class="invizo-dashboard__notice invizo-dashboard__notice--success" role="status">' . esc_html__( 'Renewal canceled. You keep access until the end of the current billing period.', 'invizo-subscriptions-lite' ) . '</div>';
        }
        if ( isset( $_GET['invizo_cancel_err'] ) ) {
            $notice .= '<div class="invizo-dashboard__notice invizo-dashboard__notice--error" role="alert">' . esc_html( sanitize_text_field( wp_unslash( $_GET['invizo_cancel_err'] ) ) ) . '</div>';
        }
        if ( isset( $_GET['invizo_pm_ok'] ) && (string) sanitize_text_field( wp_unslash( $_GET['invizo_pm_ok'] ) ) === '1' ) {
            $notice .= '<div class="invizo-dashboard__notice invizo-dashboard__notice--success" role="status">' . esc_html__( 'Primary card updated for future renewals.', 'invizo-subscriptions-lite' ) . '</div>';
        }
        if ( isset( $_GET['invizo_pm_err'] ) ) {
            $notice .= '<div class="invizo-dashboard__notice invizo-dashboard__notice--error" role="alert">' . esc_html( sanitize_text_field( wp_unslash( $_GET['invizo_pm_err'] ) ) ) . '</div>';
        }
        if (
            isset( $_GET['invizo_subscribed'] )
            && (string) sanitize_text_field( wp_unslash( $_GET['invizo_subscribed'] ) ) === '1'
            && ( ! isset( $_GET['invizo_pending_verify'] ) || (string) sanitize_text_field( wp_unslash( $_GET['invizo_pending_verify'] ) ) !== '1' )
        ) {
            $notice .= '<div class="invizo-dashboard__notice invizo-dashboard__notice--success" role="status">' . esc_html__( 'Thank you! Your subscription is active.', 'invizo-subscriptions-lite' ) . '</div>';
        }

        // phpcs:enable WordPress.Security.NonceVerification.Recommended

        switch ( $tab ) {
            case 'profile':
                $inner = self::render_profile_panel();
                break;
            case 'billing':
                $inner = self::render_billing_section( $base );
                break;
            default:
                $inner = self::render_subscription_list( $base );
                break;
        }

        return self::wrap_dashboard( $notice . $inner, $tab, $base );
    }

    /**
     * @param string $inner HTML.
     * @param string $active_tab subscriptions|profile|billing.
     * @param string $base_url  Portal base URL.
     * @return string
     */
    protected static function wrap_dashboard( $inner, $active_tab, $base_url ) {
        ob_start();
        ?>
        <div class="invizo-dashboard">
            <aside class="invizo-dashboard__nav" aria-label="<?php esc_attr_e( 'Account menu', 'invizo-subscriptions-lite' ); ?>">
                <ul class="invizo-dashboard__nav-list">
                    <li>
                        <a class="invizo-dashboard__nav-link<?php echo esc_attr( $active_tab === 'subscriptions' ? ' is-active' : '' ); ?>" href="<?php echo esc_url( remove_query_arg( array( self::QUERY_VAR ), add_query_arg( self::TAB_QUERY, 'subscriptions', $base_url ) ) ); ?>">
                            <?php esc_html_e( 'Subscriptions', 'invizo-subscriptions-lite' ); ?>
                        </a>
                    </li>
                    <li>
                        <a class="invizo-dashboard__nav-link<?php echo esc_attr( $active_tab === 'profile' ? ' is-active' : '' ); ?>" href="<?php echo esc_url( remove_query_arg( array( self::QUERY_VAR ), add_query_arg( self::TAB_QUERY, 'profile', $base_url ) ) ); ?>">
                            <?php esc_html_e( 'Account access', 'invizo-subscriptions-lite' ); ?>
                        </a>
                    </li>
                    <li>
                        <a class="invizo-dashboard__nav-link<?php echo esc_attr( $active_tab === 'billing' ? ' is-active' : '' ); ?>" href="<?php echo esc_url( remove_query_arg( array( self::QUERY_VAR ), add_query_arg( self::TAB_QUERY, 'billing', $base_url ) ) ); ?>">
                            <?php esc_html_e( 'Payment methods', 'invizo-subscriptions-lite' ); ?>
                        </a>
                    </li>
                    <li>
                        <a class="invizo-dashboard__nav-link invizo-dashboard__nav-link--logout" href="<?php echo esc_url( wp_logout_url( $base_url ) ); ?>">
                            <?php esc_html_e( 'Log out', 'invizo-subscriptions-lite' ); ?>
                        </a>
                    </li>
                </ul>
            </aside>
            <div class="invizo-dashboard__content">
                <?php echo wp_kses( $inner, self::get_dashboard_allowed_html() ); ?>
            </div>
        </div>
        <?php
        return ob_get_clean();
    }

    /**
     * Allowed HTML for assembled dashboard sections.
     *
     * @return array<string, array<string, bool>>
     */
    protected static function get_dashboard_allowed_html() {
        $allowed = wp_kses_allowed_html( 'post' );

        $allowed['a']      = array_merge( $allowed['a'], array( 'aria-label' => true, 'class' => true, 'rel' => true, 'target' => true ) );
        $allowed['aside']  = array( 'aria-label' => true, 'class' => true );
        $allowed['button'] = array( 'class' => true, 'disabled' => true, 'type' => true );
        $allowed['code']   = array( 'class' => true );
        $allowed['dd']     = array( 'class' => true );
        $allowed['div']    = array_merge( $allowed['div'], array( 'aria-hidden' => true, 'class' => true, 'role' => true ) );
        $allowed['dl']     = array( 'class' => true );
        $allowed['dt']     = array( 'class' => true );
        $allowed['form']   = array( 'action' => true, 'class' => true, 'method' => true );
        $allowed['header'] = array( 'class' => true );
        $allowed['input']  = array(
            'autocomplete' => true,
            'checked'      => true,
            'class'        => true,
            'hidden'       => true,
            'id'           => true,
            'minlength'    => true,
            'name'         => true,
            'required'     => true,
            'type'         => true,
            'value'        => true,
        );
        $allowed['label']  = array( 'class' => true, 'for' => true );
        $allowed['li']     = array_merge( $allowed['li'], array( 'class' => true ) );
        $allowed['nav']    = array( 'class' => true );
        $allowed['option'] = array( 'selected' => true, 'value' => true );
        $allowed['section'] = array(
            'aria-label'      => true,
            'aria-labelledby' => true,
            'class'           => true,
        );
        $allowed['select'] = array(
            'aria-label'   => true,
            'autocomplete' => true,
            'class'        => true,
            'id'           => true,
            'name'         => true,
        );
        $allowed['span']   = array_merge( $allowed['span'], array( 'aria-hidden' => true, 'class' => true ) );
        $allowed['time']   = array( 'class' => true, 'datetime' => true );
        $allowed['ul']     = array_merge( $allowed['ul'], array( 'class' => true ) );

        return $allowed;
    }

    /**
     * @return string
     */
    protected static function render_profile_panel() {
        $user         = wp_get_current_user();
        $login_url    = class_exists( Auth::class, false ) ? Auth::get_login_page_url() : wp_login_url( self::get_current_portal_base_url() );
        $register_url = class_exists( Auth::class, false ) ? Auth::get_register_page_url() : $login_url;
        $reset_url    = wp_lostpassword_url( $login_url );
        ob_start();
        ?>
        <div class="invizo-portal invizo-portal--profile">
            <h2 class="invizo-portal__title"><?php esc_html_e( 'Account access', 'invizo-subscriptions-lite' ); ?></h2>
            <p class="invizo-portal__lead"><?php esc_html_e( 'WordPress handles sign-in, registration, and password resets on its own secure account screens. Use the same email address you used for checkout so Invizo can match subscriptions to this account.', 'invizo-subscriptions-lite' ); ?></p>
            <dl class="invizo-portal__dl">
                <div class="invizo-portal__row">
                    <dt><?php esc_html_e( 'Display name', 'invizo-subscriptions-lite' ); ?></dt>
                    <dd><?php echo esc_html( $user->display_name ); ?></dd>
                </div>
                <div class="invizo-portal__row">
                    <dt><?php esc_html_e( 'Email', 'invizo-subscriptions-lite' ); ?></dt>
                    <dd><?php echo esc_html( $user->user_email ); ?></dd>
                </div>
            </dl>
            <p class="invizo-auth__submit">
                <a class="invizo-auth__btn invizo-auth__btn--ghost" href="<?php echo esc_url( $reset_url ); ?>"><?php esc_html_e( 'Reset password', 'invizo-subscriptions-lite' ); ?></a>
            </p>
            <?php if ( get_option( 'users_can_register' ) && $register_url !== $login_url ) : ?>
                <p class="invizo-auth__hint"><?php esc_html_e( 'Need to attach a different purchase email? Register a separate site account with that email, then log in there to see the matching subscription.', 'invizo-subscriptions-lite' ); ?></p>
            <?php endif; ?>
        </div>
        <?php
        return ob_get_clean();
    }

    /**
     * @param string $customer_id cus_xxx.
     * @return array<int, array<string, mixed>>|\WP_Error
     */
    protected static function get_stripe_card_payment_methods( $customer_id ) {
        $keys = StripeGateway::get_api_keys();
        if ( empty( $keys['secret'] ) ) {
            return new WP_Error( 'invizo_stripe', __( 'Stripe is not configured.', 'invizo-subscriptions-lite' ) );
        }

        if ( ! StripeGateway::load_sdk() ) {
            return new WP_Error( 'invizo_stripe', __( 'Stripe SDK missing.', 'invizo-subscriptions-lite' ) );
        }

        \InvizoSubscriptionsLiteVendor\Stripe\Stripe::setApiKey( $keys['secret'] );
        \InvizoSubscriptionsLiteVendor\Stripe\Stripe::setApiVersion( '2023-10-16' );

        try {
            $cust = \InvizoSubscriptionsLiteVendor\Stripe\Customer::retrieve(
                array(
                    'id'     => $customer_id,
                    'expand' => array( 'invoice_settings.default_payment_method' ),
                )
            );
            $default_id = '';
            if ( ! empty( $cust->invoice_settings->default_payment_method ) ) {
                $d = $cust->invoice_settings->default_payment_method;
                $default_id = is_string( $d ) ? $d : ( $d->id ?? '' );
            }
            $pms  = \InvizoSubscriptionsLiteVendor\Stripe\PaymentMethod::all( array( 'customer' => $customer_id, 'type' => 'card' ) );
            $rows = array();
            foreach ( $pms->data as $pm ) {
                $card      = $pm->card;
                $rows[]    = array(
                    'id'         => $pm->id,
                    'brand'      => strtoupper( (string) ( $card->brand ?? 'CARD' ) ),
                    'last4'      => (string) ( $card->last4 ?? '' ),
                    'exp_month'  => (int) ( $card->exp_month ?? 0 ),
                    'exp_year'   => (int) ( $card->exp_year ?? 0 ),
                    'is_default' => ( $pm->id === $default_id ),
                );
            }
            return $rows;
        } catch ( Exception $e ) {
            return new WP_Error( 'stripe', $e->getMessage() );
        }
    }

    /**
     * @param string $base_url Account page URL.
     * @return string
     */
    protected static function render_billing_section( $base_url ) {
        $user         = wp_get_current_user();
        $customer_id  = get_user_meta( $user->ID, 'invizo_stripe_customer_id', true );
        $portal_url   = wp_nonce_url(
            add_query_arg( 'invizo_open_stripe_portal', '1', $base_url ),
            'invizo_stripe_portal',
            'invizo_portal_nonce'
        );
        $cards        = is_string( $customer_id ) && $customer_id !== '' ? self::get_stripe_card_payment_methods( $customer_id ) : array();

        ob_start();
        ?>
        <div class="invizo-portal invizo-portal--billing">
            <h2 class="invizo-portal__title"><?php esc_html_e( 'Payment methods', 'invizo-subscriptions-lite' ); ?></h2>
            <p class="invizo-portal__lead"><?php esc_html_e( 'Add or remove cards in Stripe’s secure portal. Set which card is charged on your next renewal below.', 'invizo-subscriptions-lite' ); ?></p>
            <?php if ( $customer_id ) : ?>
                <p class="invizo-auth__submit">
                    <a class="invizo-auth__btn invizo-auth__btn--primary" href="<?php echo esc_url( $portal_url ); ?>"><?php esc_html_e( 'Manage payment methods', 'invizo-subscriptions-lite' ); ?></a>
                </p>
                <?php if ( is_wp_error( $cards ) ) : ?>
                    <p class="invizo-portal__empty"><?php echo esc_html( $cards->get_error_message() ); ?></p>
                <?php elseif ( ! empty( $cards ) ) : ?>
                    <h3 class="invizo-portal__subtitle"><?php esc_html_e( 'Primary card for renewals', 'invizo-subscriptions-lite' ); ?></h3>
                    <p class="invizo-auth__hint"><?php esc_html_e( 'We update your Stripe customer and active subscriptions so the next invoice uses this card.', 'invizo-subscriptions-lite' ); ?></p>
                    <ul class="invizo-portal__card-list">
                        <?php foreach ( $cards as $row ) : ?>
                            <li class="invizo-portal__card-row">
                                <div class="invizo-portal__card-meta">
                                    <strong><?php echo esc_html( $row['brand'] ); ?></strong>
                                    <?php esc_html_e( 'ending in', 'invizo-subscriptions-lite' ); ?>
                                    <?php echo esc_html( $row['last4'] ); ?>
                                    <?php if ( $row['exp_month'] && $row['exp_year'] ) : ?>
                                        <span class="invizo-portal__card-exp">
                                            <?php echo esc_html( sprintf( '%02d / %d', $row['exp_month'], $row['exp_year'] ) ); ?>
                                        </span>
                                    <?php endif; ?>
                                    <?php if ( ! empty( $row['is_default'] ) ) : ?>
                                        <span class="invizo-portal__badge invizo-portal__badge--default"><?php esc_html_e( 'Primary', 'invizo-subscriptions-lite' ); ?></span>
                                    <?php endif; ?>
                                </div>
                                <?php if ( empty( $row['is_default'] ) ) : ?>
                                    <form method="post" action="" class="invizo-portal__inline-form">
                                        <?php wp_nonce_field( 'invizo_default_pm', 'invizo_pm_nonce' ); ?>
                                        <input type="hidden" name="invizo_default_pm" value="1" />
                                        <input type="hidden" name="invizo_pm_id" value="<?php echo esc_attr( $row['id'] ); ?>" />
                                        <button type="submit" class="invizo-auth__btn invizo-auth__btn--ghost invizo-portal__btn-compact"><?php esc_html_e( 'Use for renewals', 'invizo-subscriptions-lite' ); ?></button>
                                    </form>
                                <?php endif; ?>
                            </li>
                        <?php endforeach; ?>
                    </ul>
                <?php else : ?>
                    <p class="invizo-portal__empty"><?php esc_html_e( 'No saved cards yet. Open “Manage payment methods” to add a card.', 'invizo-subscriptions-lite' ); ?></p>
                <?php endif; ?>
            <?php else : ?>
                <p class="invizo-portal__empty"><?php esc_html_e( 'No saved Stripe customer yet. Complete a subscription checkout first; then you can manage payment methods here.', 'invizo-subscriptions-lite' ); ?></p>
            <?php endif; ?>
        </div>
        <?php
        return ob_get_clean();
    }

    /**
     * Cancel-at-period-end and set default payment method (POST + nonce).
     */
    public static function maybe_handle_subscription_actions() {
        if ( ! is_user_logged_in() ) {
            return;
        }

        $default_pm_flag = isset( $_POST['invizo_default_pm'] ) ? sanitize_text_field( wp_unslash( $_POST['invizo_default_pm'] ) ) : '';
        if ( $default_pm_flag === '1' ) {
            if ( ! isset( $_POST['invizo_pm_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['invizo_pm_nonce'] ) ), 'invizo_default_pm' ) ) {
                return;
            }
            $pm_id       = isset( $_POST['invizo_pm_id'] ) ? sanitize_text_field( wp_unslash( $_POST['invizo_pm_id'] ) ) : '';
            $customer_id = get_user_meta( get_current_user_id(), 'invizo_stripe_customer_id', true );
            if ( ! is_string( $customer_id ) || $customer_id === '' || ! is_string( $pm_id ) || $pm_id === '' ) {
                return;
            }
            $ids    = self::get_subscription_ids_for_user( get_current_user_id() );
            $result = StripeGateway::customer_set_default_payment_method( $customer_id, $pm_id, $ids );
            $redirect = wp_get_referer();
            if ( ! $redirect ) {
                $redirect = add_query_arg( self::TAB_QUERY, 'billing', self::get_current_portal_base_url() );
            }
            $redirect = remove_query_arg( array( 'invizo_pm_ok', 'invizo_pm_err' ), $redirect );
            if ( is_wp_error( $result ) ) {
                wp_safe_redirect( add_query_arg( 'invizo_pm_err', rawurlencode( $result->get_error_message() ), $redirect ) );
            } else {
                wp_safe_redirect( add_query_arg( 'invizo_pm_ok', '1', $redirect ) );
            }
            exit;
        }

        $sub_cancel_flag = isset( $_POST['invizo_sub_cancel'] ) ? sanitize_text_field( wp_unslash( $_POST['invizo_sub_cancel'] ) ) : '';
        if ( $sub_cancel_flag === '1' ) {
            if ( ! isset( $_POST['invizo_cancel_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['invizo_cancel_nonce'] ) ), 'invizo_cancel_sub' ) ) {
                return;
            }
            $sub_id = isset( $_POST['invizo_cancel_sub_id'] ) ? absint( wp_unslash( $_POST['invizo_cancel_sub_id'] ) ) : 0;
            if ( ! self::current_user_can_view_subscription( $sub_id ) ) {
                return;
            }
            $stripe_sub = get_post_meta( $sub_id, '_stripe_subscription_id', true );
            if ( ! is_string( $stripe_sub ) || strpos( $stripe_sub, 'sub_' ) !== 0 ) {
                return;
            }
            $result = StripeGateway::subscription_cancel_at_period_end( $stripe_sub );
            StripeGateway::sync_invizo_sub_from_stripe( $sub_id );
            $redirect = wp_get_referer();
            if ( ! $redirect ) {
                $redirect = self::get_current_portal_base_url();
            }
            $redirect = remove_query_arg( array( 'invizo_cancel_ok', 'invizo_cancel_err' ), $redirect );
            if ( is_wp_error( $result ) ) {
                wp_safe_redirect( add_query_arg( 'invizo_cancel_err', rawurlencode( $result->get_error_message() ), $redirect ) );
            } else {
                wp_safe_redirect( add_query_arg( 'invizo_cancel_ok', '1', $redirect ) );
            }
            exit;
        }
    }

    public static function maybe_redirect_stripe_portal() {
        $open_portal = isset( $_GET['invizo_open_stripe_portal'] ) ? sanitize_text_field( wp_unslash( $_GET['invizo_open_stripe_portal'] ) ) : '';
        if ( $open_portal !== '1' ) {
            return;
        }
        if ( ! is_user_logged_in() ) {
            return;
        }
        if ( ! isset( $_GET['invizo_portal_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['invizo_portal_nonce'] ) ), 'invizo_stripe_portal' ) ) {
            return;
        }

        $customer_id = get_user_meta( get_current_user_id(), 'invizo_stripe_customer_id', true );
        $fail_url    = remove_query_arg(
            array( 'invizo_open_stripe_portal', 'invizo_portal_nonce' ),
            get_permalink() ? get_permalink() : Auth::get_account_page_url()
        );
        $fail_url    = add_query_arg(
            array(
                self::TAB_QUERY     => 'billing',
                'invizo_billing_err' => rawurlencode( __( 'Could not open billing portal.', 'invizo-subscriptions-lite' ) ),
            ),
            $fail_url
        );

        if ( ! $customer_id ) {
            wp_safe_redirect( $fail_url );
            exit;
        }

        $keys = StripeGateway::get_api_keys();
        if ( empty( $keys['secret'] ) ) {
            wp_safe_redirect( $fail_url );
            exit;
        }

        if ( ! StripeGateway::load_sdk() ) {
            wp_safe_redirect( $fail_url );
            exit;
        }

        \InvizoSubscriptionsLiteVendor\Stripe\Stripe::setApiKey( $keys['secret'] );

        $return_url = remove_query_arg(
            array( 'invizo_open_stripe_portal', 'invizo_portal_nonce', 'invizo_billing_err' ),
            get_permalink() ? get_permalink() : Auth::get_account_page_url()
        );
        $return_url = add_query_arg( self::TAB_QUERY, 'billing', $return_url );

        try {
            $session = \InvizoSubscriptionsLiteVendor\Stripe\BillingPortal\Session::create(
                array(
                    'customer'   => $customer_id,
                    'return_url' => $return_url,
                )
            );
        } catch ( Exception $e ) {
            $fail_url = remove_query_arg( 'invizo_billing_err', $fail_url );
            $fail_url = add_query_arg( 'invizo_billing_err', rawurlencode( $e->getMessage() ), $fail_url );
            wp_safe_redirect( $fail_url );
            exit;
        }

        wp_safe_redirect( $session->url );
        exit;
    }

    /**
     * @param int    $subscription_id invizo_sub ID.
     * @param string $base_url        List page URL.
     * @param bool   $dashboard       If true, back link preserves dashboard tab.
     * @return string
     */
    protected static function render_single_subscription( $subscription_id, $base_url, $dashboard = false ) {
        if ( ! self::current_user_can_view_subscription( $subscription_id ) ) {
            return '<div class="invizo-portal invizo-portal--notice"><p>' . esc_html__( 'You do not have access to this subscription.', 'invizo-subscriptions-lite' ) . '</p></div>';
        }

        $post = get_post( $subscription_id );
        if ( ! $post ) {
            return '<div class="invizo-portal invizo-portal--notice"><p>' . esc_html__( 'Subscription not found.', 'invizo-subscriptions-lite' ) . '</p></div>';
        }

        StripeGateway::sync_invizo_sub_from_stripe( $subscription_id );

        $plan_id     = (int) get_post_meta( $subscription_id, '_plan_id', true );
        $plan_title  = $plan_id ? get_the_title( $plan_id ) : __( 'N/A', 'invizo-subscriptions-lite' );
        $status      = ucfirst( get_post_meta( $subscription_id, '_status', true ) ?: 'active' );
        $next        = get_post_meta( $subscription_id, '_next_payment_date', true ) ?: '—';
        $expiry      = get_post_meta( $subscription_id, '_expiry_date', true ) ?: '—';
        $amount      = self::format_plan_amount( $plan_id );
        $stripe_sub  = get_post_meta( $subscription_id, '_stripe_subscription_id', true );
        $woo_order   = get_post_meta( $subscription_id, '_woo_order_id', true );
        $cancel_end  = get_post_meta( $subscription_id, '_cancel_at_period_end', true ) === '1';
        $status_lc   = strtolower( (string) get_post_meta( $subscription_id, '_status', true ) );
        $can_cancel  = is_string( $stripe_sub ) && strpos( $stripe_sub, 'sub_' ) === 0
            && ! in_array( $status_lc, array( 'canceled', 'cancelled', 'incomplete_expired' ), true )
            && ! $cancel_end;

        $back_url = remove_query_arg( self::QUERY_VAR, $base_url );
        if ( $dashboard ) {
            $back_url = add_query_arg( self::TAB_QUERY, 'subscriptions', $back_url );
        }

        ob_start();
        ?>
        <div class="invizo-portal invizo-portal--single">
            <nav class="invizo-portal__nav">
                <a class="invizo-portal__back" href="<?php echo esc_url( $back_url ); ?>">&larr; <?php esc_html_e( 'All subscriptions', 'invizo-subscriptions-lite' ); ?></a>
            </nav>
            <header class="invizo-portal__header">
                <h2 class="invizo-portal__title"><?php esc_html_e( 'Subscription details', 'invizo-subscriptions-lite' ); ?></h2>
                <span class="invizo-portal__badge invizo-portal__badge--<?php echo esc_attr( strtolower( $status ) ); ?>"><?php echo esc_html( $status ); ?></span>
            </header>
            <dl class="invizo-portal__dl">
                <div class="invizo-portal__row">
                    <dt><?php esc_html_e( 'Subscription ID', 'invizo-subscriptions-lite' ); ?></dt>
                    <dd>#<?php echo esc_html( (string) $subscription_id ); ?></dd>
                </div>
                <div class="invizo-portal__row">
                    <dt><?php esc_html_e( 'Plan', 'invizo-subscriptions-lite' ); ?></dt>
                    <dd><?php echo esc_html( $plan_title ); ?></dd>
                </div>
                <div class="invizo-portal__row">
                    <dt><?php esc_html_e( 'Recurring amount', 'invizo-subscriptions-lite' ); ?></dt>
                    <dd><?php echo esc_html( $amount ); ?></dd>
                </div>
                <div class="invizo-portal__row">
                    <dt><?php esc_html_e( 'Next payment', 'invizo-subscriptions-lite' ); ?></dt>
                    <dd><?php echo esc_html( $next ); ?></dd>
                </div>
                <div class="invizo-portal__row">
                    <dt><?php esc_html_e( 'Expiry', 'invizo-subscriptions-lite' ); ?></dt>
                    <dd><?php echo esc_html( $expiry ); ?></dd>
                </div>
                <?php if ( $stripe_sub ) : ?>
                <div class="invizo-portal__row">
                    <dt><?php esc_html_e( 'Stripe subscription', 'invizo-subscriptions-lite' ); ?></dt>
                    <dd><code class="invizo-portal__code"><?php echo esc_html( $stripe_sub ); ?></code></dd>
                </div>
                <?php endif; ?>
                <?php if ( $woo_order && function_exists( 'wc_get_order' ) ) : ?>
                    <?php
                    $order = wc_get_order( $woo_order );
                    $order_url = $order ? $order->get_view_order_url() : '';
                    ?>
                <div class="invizo-portal__row">
                    <dt><?php esc_html_e( 'Order', 'invizo-subscriptions-lite' ); ?></dt>
                    <dd>
                        <?php if ( $order_url ) : ?>
                            <a href="<?php echo esc_url( $order_url ); ?>">#<?php echo esc_html( (string) $woo_order ); ?></a>
                        <?php else : ?>
                            #<?php echo esc_html( (string) $woo_order ); ?>
                        <?php endif; ?>
                    </dd>
                </div>
                <?php endif; ?>
            </dl>
            <?php if ( $cancel_end ) : ?>
                <p class="invizo-portal__muted"><?php esc_html_e( 'Auto-renewal is turned off. You keep access until the end of the current period.', 'invizo-subscriptions-lite' ); ?></p>
            <?php endif; ?>
            <?php if ( $can_cancel ) : ?>
                <form method="post" action="" class="invizo-portal__cancel-form" onsubmit="return confirm('<?php echo esc_js( __( 'Cancel auto-renewal at the end of this billing period? You will keep access until then.', 'invizo-subscriptions-lite' ) ); ?>');">
                    <?php wp_nonce_field( 'invizo_cancel_sub', 'invizo_cancel_nonce' ); ?>
                    <input type="hidden" name="invizo_sub_cancel" value="1" />
                    <input type="hidden" name="invizo_cancel_sub_id" value="<?php echo esc_attr( (string) $subscription_id ); ?>" />
                    <button type="submit" class="invizo-auth__btn invizo-auth__btn--ghost"><?php esc_html_e( 'Cancel renewal', 'invizo-subscriptions-lite' ); ?></button>
                </form>
            <?php endif; ?>
        </div>
        <?php
        return ob_get_clean();
    }

    /**
     * @param string $base_url Portal base URL.
     * @return string
     */
    protected static function render_subscription_list( $base_url ) {
        $ids = self::get_subscription_ids_for_user( get_current_user_id() );

        ob_start();
        ?>
        <div class="invizo-portal invizo-portal--list">
            <h2 class="invizo-portal__title"><?php esc_html_e( 'My subscriptions', 'invizo-subscriptions-lite' ); ?></h2>
            <?php if ( empty( $ids ) ) : ?>
                <p class="invizo-portal__empty"><?php esc_html_e( 'You have no subscriptions yet.', 'invizo-subscriptions-lite' ); ?></p>
            <?php else : ?>
                <div class="invizo-portal__table-wrap">
                    <table class="invizo-portal__table">
                        <thead>
                            <tr>
                                <th><?php esc_html_e( 'Plan', 'invizo-subscriptions-lite' ); ?></th>
                                <th><?php esc_html_e( 'Status', 'invizo-subscriptions-lite' ); ?></th>
                                <th><?php esc_html_e( 'Amount', 'invizo-subscriptions-lite' ); ?></th>
                                <th><?php esc_html_e( 'Next payment', 'invizo-subscriptions-lite' ); ?></th>
                                <th></th>
                            </tr>
                        </thead>
                        <tbody>
                            <?php foreach ( $ids as $sid ) : ?>
                                <?php
                                if ( ! get_post_meta( $sid, '_next_payment_date', true ) ) {
                                    StripeGateway::sync_invizo_sub_from_stripe( $sid );
                                }
                                $plan_id    = (int) get_post_meta( $sid, '_plan_id', true );
                                $title      = $plan_id ? get_the_title( $plan_id ) : '—';
                                $status_raw = get_post_meta( $sid, '_status', true ) ?: 'active';
                                $status     = ucfirst( $status_raw );
                                $status_lc  = strtolower( (string) $status_raw );
                                $next       = get_post_meta( $sid, '_next_payment_date', true ) ?: '—';
                                $amount     = self::format_plan_amount( $plan_id );
                                $view       = add_query_arg( self::QUERY_VAR, $sid, $base_url );
                                $stripe_sub = get_post_meta( $sid, '_stripe_subscription_id', true );
                                $cancel_end = get_post_meta( $sid, '_cancel_at_period_end', true ) === '1';
                                $can_cancel = is_string( $stripe_sub ) && strpos( $stripe_sub, 'sub_' ) === 0
                                    && ! in_array( $status_lc, array( 'canceled', 'cancelled', 'incomplete_expired' ), true )
                                    && ! $cancel_end;
                                ?>
                                <tr>
                                    <td data-label="<?php esc_attr_e( 'Plan', 'invizo-subscriptions-lite' ); ?>"><?php echo esc_html( $title ); ?></td>
                                    <td data-label="<?php esc_attr_e( 'Status', 'invizo-subscriptions-lite' ); ?>">
                                        <span class="invizo-portal__badge invizo-portal__badge--<?php echo esc_attr( strtolower( $status ) ); ?>"><?php echo esc_html( $status ); ?></span>
                                        <?php if ( $cancel_end ) : ?>
                                            <span class="invizo-portal__sub-note"><?php esc_html_e( '(renewal off)', 'invizo-subscriptions-lite' ); ?></span>
                                        <?php endif; ?>
                                    </td>
                                    <td data-label="<?php esc_attr_e( 'Amount', 'invizo-subscriptions-lite' ); ?>"><?php echo esc_html( $amount ); ?></td>
                                    <td data-label="<?php esc_attr_e( 'Next payment', 'invizo-subscriptions-lite' ); ?>"><?php echo esc_html( $next ); ?></td>
                                    <td class="invizo-portal__actions">
                                        <a class="invizo-portal__link" href="<?php echo esc_url( $view ); ?>"><?php esc_html_e( 'View', 'invizo-subscriptions-lite' ); ?></a>
                                        <?php if ( $can_cancel ) : ?>
                                            <form method="post" action="" class="invizo-portal__inline-form invizo-portal__inline-form--stack" onsubmit="return confirm('<?php echo esc_js( __( 'Cancel auto-renewal at the end of this billing period?', 'invizo-subscriptions-lite' ) ); ?>');">
                                                <?php wp_nonce_field( 'invizo_cancel_sub', 'invizo_cancel_nonce' ); ?>
                                                <input type="hidden" name="invizo_sub_cancel" value="1" />
                                                <input type="hidden" name="invizo_cancel_sub_id" value="<?php echo esc_attr( (string) $sid ); ?>" />
                                                <button type="submit" class="invizo-portal__link invizo-portal__link--btn"><?php esc_html_e( 'Cancel renewal', 'invizo-subscriptions-lite' ); ?></button>
                                            </form>
                                        <?php endif; ?>
                                    </td>
                                </tr>
                            <?php endforeach; ?>
                        </tbody>
                    </table>
                </div>
            <?php endif; ?>
        </div>
        <?php
        return ob_get_clean();
    }
}
