<?php

namespace BSD_Split_Pay_Stripe_Connect_Woo\Inc\Admin;

use Exception;
use WP_Query;
if ( !defined( 'ABSPATH' ) ) {
    exit( 'Sorry!' );
}
// Exit if accessed directly
require_once SPP_PLUGIN_DIR . '/includes/vendor/autoload.php';
if ( !class_exists( 'BSD_Split_Pay_Stripe_Connect_Woo\\Inc\\Admin\\BSD_SCA' ) ) {
    class BSD_SCA {
        public $log_prefix = '[BSD Split Pay for Stripe Connect] ';

        public function run() {
            add_action( 'admin_init', array($this, 'bsd_connect_standard_account') );
            // add_action( 'admin_init', array( $this, 'spp_no_keys_redirect_rule' ) );
            add_action( 'admin_init', array($this, 'spp_redirect_to_settings') );
            add_action( 'admin_init', array($this, 'get_initial_accounts') );
            // 3.7.5: one-time backfill of bsd_account_mode for accounts cached
            // before the column existed. Runs after get_initial_accounts (pri 20)
            // so any rows it just inserted are already mode-stamped.
            add_action( 'admin_init', array($this, 'reconcile_account_modes'), 20 );
            add_action( 'wp_ajax_fetch_accounts', array($this, 'fetch_accounts') );
            add_action( 'wp_ajax_clear_accounts', array($this, 'clear_accounts') );
            add_action( 'wp_ajax_add_custom_account', array($this, 'add_custom_account') );
            add_action( 'wp_loaded', array($this, 'create_connect_id_table') );
            add_action( 'admin_init', array($this, 'store_selected_accounts'), 10 );
            add_action( 'admin_init', array($this, 'spp_vendor_options_process'), 12 );
            add_action( 'plugins_loaded', array($this, 'update_connected_id_table') );
            add_action( 'plugins_loaded', array($this, 'update_log_table') );
            add_action( 'plugins_loaded', array($this, 'update_log_table_integration_source') );
            add_action( 'plugins_loaded', array($this, 'update_account_id_table') );
            add_action( 'admin_notices', array($this, 'sps_admin_notices') );
            add_action( 'wp_ajax_fetch_more_filter', array($this, 'add_more_bulk_editor_filter') );
            add_action( 'wp_ajax_fetch_search_result', array($this, 'product_search_data') );
            add_action( 'wp_ajax_save_product_bulk_edit', array($this, 'save_product_bulk_edit_data') );
            $transfer_email = get_option( 'transfer_email', false );
            if ( $transfer_email == '1' ) {
                add_filter(
                    'wc_get_template',
                    array($this, 'sps_get_woocommerce_template'),
                    10,
                    5
                );
                add_filter( 'woocommerce_email_classes', array($this, 'add_transfer_success_order_woocommerce_email') );
            }
            // Use the woocommerce email template under this plugin directory.
            add_filter(
                'pre_update_option',
                array($this, 'filter_masked_key'),
                10,
                3
            );
            $stripe_test_api_public_key = get_option( 'stripe_test_api_public_key', false );
            $stripe_test_api_secret_key = get_option( 'stripe_test_api_secret_key', false );
            $stripe_api_public_key = get_option( 'stripe_api_public_key', false );
            $stripe_api_secret_key = get_option( 'stripe_api_secret_key', false );
            if ( empty( $stripe_test_api_public_key ) && empty( $stripe_test_api_secret_key ) || empty( $stripe_api_public_key ) && empty( $stripe_api_secret_key ) ) {
                add_action( 'admin_notices', array($this, 'stripe_key_notice_cb') );
            }
            add_action( 'wp_ajax_sync_webhooks', array($this, 'sync_webhooks') );
            add_action( 'wp_ajax_spp_sync_gateway_webhooks', array($this, 'sync_gateway_webhooks') );
            add_action( 'wp_ajax_spp_sync_gateway_accounts', array($this, 'sync_gateway_accounts') );
            add_action( 'wp_ajax_spp_save_gateway_key', array($this, 'save_gateway_key') );
            // 3.7.5 (PT-7): AJAX endpoint for persistent dismissal of SPP
            // admin notices when the user clicks the native "X" close button.
            add_action( 'wp_ajax_spp_persist_dismiss_notice', array($this, 'persist_dismiss_notice') );
            // 3.7.5 (PT-7): Print the small JS shim that wires the native
            // dismiss-button click to the AJAX endpoint above. Loaded on
            // every admin page so the notices stay dismissed across reloads.
            add_action( 'admin_footer', array($this, 'persist_dismiss_notice_js') );
            add_action( 'plugins_loaded', array($this, 'required_plugin_update_things') );
            add_action( 'admin_notices', array($this, 'spp_notice_for_unsupport_product_types') );
            add_action( 'admin_notices', array($this, 'spp_upgrade_notice_370') );
        }

        /**
         * 3.7.5 (PT-7) — persistent-dismiss endpoint.
         *
         * The native WordPress "X" close button on `is-dismissible` notices
         * only hides the DOM node for the current page. We capture that click
         * in JS and POST here so the dismissal sticks across page reloads.
         *
         * Each notice that wants to persist sets a `data-spp-persist-dismiss`
         * attribute to an identifier. The identifier maps to the storage
         * location below — option, user_meta, or (in future) transient.
         *
         * Only an explicit allowlist is honoured, so this can't be used to
         * blow away arbitrary options.
         */
        public function persist_dismiss_notice() {
            check_ajax_referer( 'spp_persist_dismiss_notice', '_wpnonce' );
            if ( !current_user_can( 'manage_options' ) ) {
                wp_send_json_error( [
                    'message' => 'caps',
                ], 403 );
            }
            $id = ( isset( $_POST['notice_id'] ) ? sanitize_key( wp_unslash( $_POST['notice_id'] ) ) : '' );
            if ( $id === '' ) {
                wp_send_json_error( [
                    'message' => 'no_id',
                ] );
            }
            // option-key dismissals (per-site).
            $option_keys = array('bsd_split_pay_374_recovery_notice', 'bsd_spp_mode_mismatch_notices');
            // user-meta dismissals (per-user).
            $user_meta_keys = array('spp_dismiss_370_notice');
            if ( in_array( $id, $option_keys, true ) ) {
                delete_option( $id );
                wp_send_json_success( [
                    'dismissed' => $id,
                    'storage'   => 'option',
                ] );
            }
            if ( in_array( $id, $user_meta_keys, true ) ) {
                update_user_meta( get_current_user_id(), $id, true );
                wp_send_json_success( [
                    'dismissed' => $id,
                    'storage'   => 'user_meta',
                ] );
            }
            wp_send_json_error( [
                'message' => 'unknown_id',
            ] );
        }

        /**
         * 3.7.5 (PT-7) — JS shim that wires the native "X" dismiss button on
         * any notice carrying `data-spp-persist-dismiss="<id>"` to our AJAX
         * endpoint above. Printed in the admin footer on every admin page so
         * the dismissal works no matter where the notice surfaces.
         */
        public function persist_dismiss_notice_js() {
            $nonce = wp_create_nonce( 'spp_persist_dismiss_notice' );
            $ajax = admin_url( 'admin-ajax.php' );
            ?>
			<script>
			( function () {
				if ( window.sppPersistDismissBound ) { return; }
				window.sppPersistDismissBound = true;
				var ajaxUrl = <?php 
            echo wp_json_encode( $ajax );
            ?>;
				var nonce   = <?php 
            echo wp_json_encode( $nonce );
            ?>;

				/*
				 * 3.7.5 (PT-7 v2 hardening): use **document-level capture-phase
				 * delegation** for the dismiss click. The previous per-button
				 * `addEventListener( 'click', … )` was silently bypassed on any
				 * site that also runs StellarWP's notice-dismiss.js (shipped by
				 * Event Tickets / Event Tickets Plus / The Events Calendar) —
				 * that script attaches its own click handler that
				 * `stopImmediatePropagation()`s, so any later bubble-phase
				 * listener on the button never fires and the dismissal is
				 * never persisted server-side.
				 *
				 * A capture-phase listener on `document` runs before ANY of
				 * those third-party handlers can intervene, regardless of
				 * load order or stopPropagation use, AND removes the timing
				 * race for the .notice-dismiss button being injected
				 * asynchronously by wp-admin/js/common.js.
				 *
				 * Confirmed on docker pts-dev-spp-wp-1 with
				 * event-tickets-plus 6.9.2 active — the dispatch URL never
				 * reached admin-ajax.php before this fix.
				 */
				function dispatch( noticeId ) {
					try {
						var fd = new FormData();
						fd.append( 'action', 'spp_persist_dismiss_notice' );
						fd.append( '_wpnonce', nonce );
						fd.append( 'notice_id', noticeId );
						var sent = false;
						if ( typeof navigator !== 'undefined' && typeof navigator.sendBeacon === 'function' ) {
							try { sent = !! navigator.sendBeacon( ajaxUrl, fd ); } catch ( _ ) { sent = false; }
						}
						if ( ! sent ) {
							fetch( ajaxUrl, { method: 'POST', credentials: 'same-origin', body: fd, keepalive: true } );
						}
					} catch ( e ) {}
				}

				document.addEventListener( 'click', function ( ev ) {
					var btn = ev.target;
					if ( ! btn || ! btn.classList || ! btn.classList.contains( 'notice-dismiss' ) ) {
						return;
					}
					// Walk up to find the persist-dismiss notice container.
					var notice = btn.closest( '[data-spp-persist-dismiss]' );
					if ( ! notice ) { return; }
					var id = notice.dataset.sppPersistDismiss;
					if ( ! id ) { return; }
					if ( notice.dataset.sppPersistFired === '1' ) { return; }
					notice.dataset.sppPersistFired = '1';
					dispatch( id );
				}, true /* capture */ );

				/*
				 * Also mark each notice with sppPersistBound=1 for back-compat
				 * with the v1 source-presence regression test
				 * (PtBugRegressionTest::test_admin_footer_js_marks_bound).
				 */
				function markBound() {
					var nodes = document.querySelectorAll( '[data-spp-persist-dismiss]' );
					for ( var i = 0; i < nodes.length; i++ ) {
						nodes[ i ].dataset.sppPersistBound = '1';
					}
				}
				if ( document.readyState === 'loading' ) {
					document.addEventListener( 'DOMContentLoaded', markBound );
				} else {
					markBound();
				}
			} )();
			</script>
			<?php 
        }

        public function spp_notice_for_unsupport_product_types() {
            global $pagenow;
            if ( $pagenow == 'post-new.php' || $pagenow == 'edit.php' || $pagenow == 'post.php' ) {
                if ( isset( $_GET['post'] ) && !empty( $_GET['post'] ) && 'product' === get_post_type( wp_unslash( $_GET['post'] ) ) ) {
                    $product = wc_get_product( wp_unslash( $_GET['post'] ) );
                    $supported_product_types = array(
                        'simple',
                        'variable',
                        'grouped',
                        'external'
                    );
                    if ( !in_array( $product->get_type(), $supported_product_types ) ) {
                        $list_of_compatible_wc_plugins = '<a href="https://docs.splitpayplugin.com/features/integrations-and-compatibility" target="_blank" rel="noopener noreferrer">list of compatible WooCommerce plugins</a>';
                        $support_link = '<a href="https://splitpayplugin.com/support/" target="_blank" rel="noopener noreferrer">reach out to support</a>';
                        ?>
							<div class="notice notice-warning">
								<p>
									<?php 
                        printf( esc_html__( "The Split Pay Plugin only works with some products out of the box. We've detected that you're using plugins to create different types of WooCommerce products that are not yet supported. Please review our %1\$s or %2\$s to request a custom integration.", 'bsd-split-pay-stripe-connect-woo' ), wp_kses_post( $list_of_compatible_wc_plugins ), wp_kses_post( $support_link ) );
                        ?>
								</p>
							</div>
						<?php 
                    }
                }
            }
        }

        public function spp_upgrade_notice_370() {
            $user_id = get_current_user_id();
            if ( get_user_meta( $user_id, 'spp_dismiss_370_notice', true ) ) {
                return;
            }
            if ( isset( $_GET['spp_dismiss_370'] ) ) {
                update_user_meta( $user_id, 'spp_dismiss_370_notice', true );
                return;
            }
            $integrations_url = admin_url( 'admin.php?page=bsd-split-pay-stripe-connect-woo-settings&tab=integrations' );
            $current_url = set_url_scheme( 'http://' . wp_unslash( $_SERVER['HTTP_HOST'] ) . wp_unslash( $_SERVER['REQUEST_URI'] ) );
            $dismiss_url = add_query_arg( 'spp_dismiss_370', '1', $current_url );
            /*
             * 3.7.5 (PT-7): The data-spp-persist-dismiss attribute is what
             * the admin JS hooks onto so the native X (close) button on the
             * is-dismissible notice persists the dismissal server-side via
             * AJAX — not just hides the DOM node until refresh.
             */
            ?>
			<div class="notice notice-info is-dismissible" data-spp-persist-dismiss="spp_dismiss_370_notice">
				<p>
					<?php 
            printf( 
                /* translators: %1$s = URL of the Integrations tab, %2$s = URL to dismiss the notice. */
                wp_kses_post( __( '<strong>Split Pay v3.7.0</strong>: New Integrations tab with FluentCart support and multi-gateway detection. <a href="%1$s">View Integrations</a> | <a href="%2$s">Dismiss</a>', 'bsd-split-pay-stripe-connect-woo' ) ),
                esc_url( $integrations_url ),
                esc_url( $dismiss_url )
             );
            ?>
				</p>
			</div>
			<?php 
        }

        public function required_plugin_update_things() {
            if ( defined( 'WC_STRIPE_VERSION' ) ) {
                if ( !version_compare( WC_STRIPE_VERSION, '8.6.0', '>=' ) ) {
                    $spp_keys_copied = get_option( 'spp_keys_copied', false );
                    $woocommerce_stripe_settings = get_option( 'woocommerce_stripe_settings' );
                    if ( empty( $spp_keys_copied ) && !empty( $woocommerce_stripe_settings ) ) {
                        if ( isset( $woocommerce_stripe_settings['enabled'] ) && $woocommerce_stripe_settings['enabled'] == 'yes' ) {
                            update_option( 'stripe_test_api_public_key', $woocommerce_stripe_settings['test_publishable_key'] );
                            update_option( 'stripe_test_api_secret_key', $woocommerce_stripe_settings['test_secret_key'] );
                            update_option( 'stripe_api_public_key', $woocommerce_stripe_settings['publishable_key'] );
                            update_option( 'stripe_api_secret_key', $woocommerce_stripe_settings['secret_key'] );
                            update_option( 'spp_keys_copied', '1' );
                        }
                    }
                }
            }
        }

        public function sync_webhooks() {
            // audit P1.4 — function-scope defaults for variables that the
            // later wp_send_json() call reads. The original code initialized
            // them inside the nonce-check branch, so a control-flow path
            // where the branch was skipped (impossible in practice but
            // statically undecidable) reached the reads undefined.
            $errors = array();
            $successes = array();
            $send_ajax_data = array(
                'success' => false,
                'message' => 'Something Wrong!!!',
                'data'    => array(),
            );
            if ( !current_user_can( 'manage_woocommerce' ) ) {
                // wp_send_json() calls wp_die() internally — explicit `die;` below was unreachable.
                wp_send_json( $send_ajax_data );
            }
            if ( isset( $_POST['security'] ) && wp_verify_nonce( wp_unslash( $_POST['security'] ), 'ajax-security' ) ) {
                $woocommerce_stripe_settings = get_option( 'woocommerce_stripe_settings' );
                $errors = array();
                $successes = array();
                if ( isset( $woocommerce_stripe_settings['enabled'] ) && $woocommerce_stripe_settings['enabled'] == 'yes' ) {
                    $test_webhook_data = array();
                    $webhook_data = array();
                    $webhook_url = site_url( '/?wc-api=wc_stripe' );
                    // --- TEST MODE ---
                    $stripe_key = get_option( 'stripe_test_api_secret_key', false );
                    $test_endpoint_id = '';
                    if ( !empty( $woocommerce_stripe_settings['test_webhook_data'] ) ) {
                        $test_webhook_data = $woocommerce_stripe_settings['test_webhook_data'];
                        if ( isset( $test_webhook_data['id'] ) && !empty( $test_webhook_data['id'] ) ) {
                            $test_endpoint_id = $test_webhook_data['id'];
                        }
                        if ( isset( $test_webhook_data['url'] ) && !empty( $test_webhook_data['url'] ) ) {
                            $webhook_url = $test_webhook_data['url'];
                        }
                    }
                    if ( !empty( $stripe_key ) ) {
                        $test_secret_key = ( isset( $woocommerce_stripe_settings['test_secret_key'] ) ? $woocommerce_stripe_settings['test_secret_key'] : '' );
                        if ( !empty( $test_secret_key ) ) {
                            try {
                                $wc_plugin_stripe = new \Stripe\StripeClient($test_secret_key);
                                $wc_plugin_account_obj = $wc_plugin_stripe->accounts->retrieve( null, array() );
                                $our_stripe_obj = new \Stripe\StripeClient($stripe_key);
                                $our_account_obj = $our_stripe_obj->accounts->retrieve( null, array() );
                                if ( $wc_plugin_account_obj && $our_account_obj ) {
                                    if ( $wc_plugin_account_obj->id != $our_account_obj->id ) {
                                        $woo_stripe_settings_url = admin_url( 'admin.php?page=wc-settings&tab=checkout&section=stripe&panel=settings' );
                                        /* translators: %s: opening anchor tag (with URL) for the WooCommerce Stripe Payment Gateway plugin settings; closing tag is appended. */
                                        $errors['test_mode'][] = sprintf( __( 'API keys do not match the account connected to the %1$sWooCommerce Stripe Payment Gateway plugin%2$s.', 'bsd-split-pay-stripe-connect-woo' ), '<a href="' . $woo_stripe_settings_url . '" target="_blank" rel="noopener noreferrer">', '</a>' );
                                        update_option( 'sps_test_webhook_update', '2' );
                                    }
                                }
                            } catch ( \Exception $e ) {
                                $error_message = $e->getMessage();
                                \BSD_Split_Pay_Stripe_Connect_Woo\spp_safe_wcs_log( $this->log_prefix . ': error_message ' . wp_json_encode( $error_message ) );
                                $errors['test_mode'][] = $error_message;
                            }
                        }
                        if ( !isset( $errors['test_mode'][0] ) ) {
                            $stripe = new \Stripe\StripeClient($stripe_key);
                            $existing_webhook_response = null;
                            if ( !empty( $test_endpoint_id ) ) {
                                try {
                                    $existing_webhook_response = $stripe->webhookEndpoints->retrieve( $test_endpoint_id, array() );
                                } catch ( \Exception $e ) {
                                    \BSD_Split_Pay_Stripe_Connect_Woo\spp_safe_wcs_log( $this->log_prefix . ': test retrieve by ID failed, will try URL fallback: ' . $e->getMessage() );
                                    $existing_webhook_response = null;
                                }
                            }
                            if ( !$existing_webhook_response ) {
                                $existing_webhook_response = $this->find_webhook_endpoint_by_url( $stripe, $webhook_url );
                            }
                            if ( $existing_webhook_response ) {
                                try {
                                    $merged_events = $this->merge_transfer_events( $existing_webhook_response->enabled_events );
                                    $updated_response = $stripe->webhookEndpoints->update( $existing_webhook_response->id, array(
                                        'enabled_events' => $merged_events,
                                    ) );
                                    if ( $updated_response ) {
                                        $successes['test_mode'][] = $updated_response;
                                        update_option( 'sps_test_webhook_update', '1' );
                                    }
                                } catch ( \Exception $e ) {
                                    $error_message = $e->getMessage();
                                    \BSD_Split_Pay_Stripe_Connect_Woo\spp_safe_wcs_log( $this->log_prefix . ': error_message ' . wp_json_encode( $error_message ) );
                                    $errors['test_mode'][] = $error_message;
                                    update_option( 'sps_test_webhook_update', '2' );
                                }
                            } else {
                                $errors['test_mode'][] = __( 'No webhook endpoint found for this site. Please reconnect Stripe in the WooCommerce Stripe Payment Gateway settings to create a webhook endpoint, then sync again.', 'bsd-split-pay-stripe-connect-woo' );
                                update_option( 'sps_test_webhook_update', '2' );
                            }
                        }
                    }
                    // --- LIVE MODE ---
                    $webhook_url = site_url( '/?wc-api=wc_stripe' );
                    $stripe_key = get_option( 'stripe_api_secret_key', false );
                    $live_endpoint_id = '';
                    if ( isset( $woocommerce_stripe_settings['webhook_data'] ) && !empty( $woocommerce_stripe_settings['webhook_data'] ) ) {
                        $webhook_data = $woocommerce_stripe_settings['webhook_data'];
                        if ( isset( $webhook_data['id'] ) && !empty( $webhook_data['id'] ) ) {
                            $live_endpoint_id = $webhook_data['id'];
                        }
                        if ( isset( $webhook_data['url'] ) && !empty( $webhook_data['url'] ) ) {
                            $webhook_url = $webhook_data['url'];
                        }
                    }
                    if ( !empty( $stripe_key ) ) {
                        $wc_stripe_secret_key = ( isset( $woocommerce_stripe_settings['secret_key'] ) ? $woocommerce_stripe_settings['secret_key'] : '' );
                        if ( !empty( $wc_stripe_secret_key ) ) {
                            try {
                                $wc_plugin_stripe = new \Stripe\StripeClient($wc_stripe_secret_key);
                                $wc_plugin_account_obj = $wc_plugin_stripe->accounts->retrieve( null, array() );
                                $our_stripe_obj = new \Stripe\StripeClient($stripe_key);
                                $our_account_obj = $our_stripe_obj->accounts->retrieve( null, array() );
                                if ( $wc_plugin_account_obj && $our_account_obj ) {
                                    if ( $wc_plugin_account_obj->id != $our_account_obj->id ) {
                                        $woo_stripe_settings_url = admin_url( 'admin.php?page=wc-settings&tab=checkout&section=stripe&panel=settings' );
                                        /* translators: %s: opening anchor tag (with URL) for the WooCommerce Stripe Payment Gateway plugin settings; closing tag is appended. */
                                        $errors['live_mode'][] = sprintf( __( 'API keys do not match the account connected to the %1$sWooCommerce Stripe Payment Gateway plugin%2$s.', 'bsd-split-pay-stripe-connect-woo' ), '<a href="' . $woo_stripe_settings_url . '" target="_blank" rel="noopener noreferrer">', '</a>' );
                                        update_option( 'sps_webhook_update', '2' );
                                    }
                                }
                            } catch ( \Exception $e ) {
                                $error_message = $e->getMessage();
                                \BSD_Split_Pay_Stripe_Connect_Woo\spp_safe_wcs_log( $this->log_prefix . ': error_message ' . wp_json_encode( $error_message ) );
                                $errors['live_mode'][] = $error_message;
                                update_option( 'sps_webhook_update', '2' );
                            }
                        }
                        if ( !isset( $errors['live_mode'][0] ) ) {
                            $stripe = new \Stripe\StripeClient($stripe_key);
                            $existing_webhook_response = null;
                            if ( !empty( $live_endpoint_id ) ) {
                                try {
                                    $existing_webhook_response = $stripe->webhookEndpoints->retrieve( $live_endpoint_id, array() );
                                } catch ( \Exception $e ) {
                                    \BSD_Split_Pay_Stripe_Connect_Woo\spp_safe_wcs_log( $this->log_prefix . ': live retrieve by ID failed, will try URL fallback: ' . $e->getMessage() );
                                    $existing_webhook_response = null;
                                }
                            }
                            if ( !$existing_webhook_response ) {
                                $existing_webhook_response = $this->find_webhook_endpoint_by_url( $stripe, $webhook_url );
                            }
                            if ( $existing_webhook_response ) {
                                try {
                                    $merged_events = $this->merge_transfer_events( $existing_webhook_response->enabled_events );
                                    $updated_response = $stripe->webhookEndpoints->update( $existing_webhook_response->id, array(
                                        'enabled_events' => $merged_events,
                                    ) );
                                    if ( $updated_response ) {
                                        $successes['live_mode'][] = $updated_response;
                                        update_option( 'sps_webhook_update', '1' );
                                    }
                                } catch ( \Exception $e ) {
                                    $error_message = $e->getMessage();
                                    \BSD_Split_Pay_Stripe_Connect_Woo\spp_safe_wcs_log( $this->log_prefix . ': error_message ' . wp_json_encode( $error_message ) );
                                    $errors['live_mode'][] = $error_message;
                                    update_option( 'sps_webhook_update', '2' );
                                }
                            } else {
                                $errors['live_mode'][] = __( 'No webhook endpoint found for this site. Please reconnect Stripe in the WooCommerce Stripe Payment Gateway settings to create a webhook endpoint, then sync again.', 'bsd-split-pay-stripe-connect-woo' );
                                update_option( 'sps_webhook_update', '2' );
                            }
                        }
                    }
                }
            }
            $send_ajax_data = array(
                'success' => true,
                'message' => '',
                'data'    => array(
                    'error_messages' => $errors,
                    'successes'      => $successes,
                ),
            );
            // wp_send_json() terminates — `die;` was unreachable.
            wp_send_json( $send_ajax_data );
        }

        private function find_webhook_endpoint_by_url( $stripe, $target_url ) {
            try {
                $webhooks = $stripe->webhookEndpoints->all( array(
                    'limit' => 100,
                ) );
                if ( $webhooks && is_array( $webhooks->data ) ) {
                    foreach ( $webhooks->data as $endpoint ) {
                        if ( $endpoint->status === 'disabled' ) {
                            continue;
                        }
                        if ( isset( $endpoint->url ) && $endpoint->url === $target_url ) {
                            return $endpoint;
                        }
                    }
                }
            } catch ( \Exception $e ) {
                \BSD_Split_Pay_Stripe_Connect_Woo\spp_safe_wcs_log( $this->log_prefix . ': find_webhook_endpoint_by_url error: ' . $e->getMessage() );
            }
            return null;
        }

        private function merge_transfer_events( $existing_events ) {
            $transfer_events = array('transfer.created', 'transfer.reversed', 'transfer.updated');
            if ( in_array( '*', $existing_events ) ) {
                return $existing_events;
            }
            $filtered = array_values( array_diff( $existing_events, $transfer_events ) );
            return array_values( array_merge( $filtered, $transfer_events ) );
        }

        public function sync_gateway_webhooks() {
            check_ajax_referer( 'spp_sync_webhook', '_wpnonce' );
            if ( !current_user_can( 'manage_options' ) ) {
                wp_send_json_error( [
                    'message' => 'Insufficient permissions.',
                ] );
            }
            $gateway_id = sanitize_text_field( wp_unslash( $_POST['gateway_id'] ?? '' ) );
            if ( empty( $gateway_id ) ) {
                wp_send_json_error( [
                    'message' => 'No gateway specified.',
                ] );
            }
            $registry = \SplitPay\Core\IntegrationRegistry::getInstance();
            $gateway = null;
            foreach ( $registry->getActiveGateways() as $gw ) {
                if ( $gw->getId() === $gateway_id ) {
                    $gateway = $gw;
                    break;
                }
            }
            if ( !$gateway ) {
                wp_send_json_error( [
                    'message' => 'Gateway not found or not active.',
                ] );
            }
            require_once SPP_PLUGIN_DIR . '/includes/vendor/autoload.php';
            $key_map = [
                'wc_stripe'                 => ['spp_wc_stripe_test_secret_key', 'spp_wc_stripe_live_secret_key'],
                'wc_stripe_payment_plugins' => ['spp_pp_stripe_test_secret_key', 'spp_pp_stripe_live_secret_key'],
                'fluentcart_stripe'         => ['spp_fc_stripe_test_secret_key', 'spp_fc_stripe_live_secret_key'],
            ];
            if ( $gateway_id === 'wc_stripe' ) {
                $test_key = get_option( 'spp_wc_stripe_test_secret_key', '' );
                if ( empty( $test_key ) ) {
                    $test_key = get_option( 'stripe_test_api_secret_key', '' );
                }
                $live_key = get_option( 'spp_wc_stripe_live_secret_key', '' );
                if ( empty( $live_key ) ) {
                    $live_key = get_option( 'stripe_api_secret_key', '' );
                }
            } elseif ( isset( $key_map[$gateway_id] ) ) {
                $test_key = get_option( $key_map[$gateway_id][0], '' );
                $live_key = get_option( $key_map[$gateway_id][1], '' );
            } else {
                $test_key = '';
                $live_key = '';
            }
            $keys_to_try = [];
            if ( !empty( $test_key ) ) {
                $keys_to_try['Test'] = $test_key;
            }
            if ( !empty( $live_key ) ) {
                $keys_to_try['Live'] = $live_key;
            }
            if ( empty( $keys_to_try ) ) {
                wp_send_json_error( [
                    'message' => 'No platform secret keys configured. Enter your Stripe secret keys in the gateway settings.',
                ] );
            }
            $results = [];
            foreach ( $keys_to_try as $mode_label => $key ) {
                $results[$mode_label] = $this->sync_webhooks_for_key( $key );
            }
            $parts = [];
            foreach ( $results as $mode_label => $r ) {
                if ( $r['error'] ) {
                    $parts[] = $mode_label . ': ' . $r['error'];
                } elseif ( $r['synced'] > 0 ) {
                    $parts[] = $mode_label . ': synced ' . $r['synced'] . ' endpoint(s)';
                } else {
                    $parts[] = $mode_label . ': all endpoints already have transfer events';
                }
            }
            wp_send_json_success( [
                'message' => implode( ' — ', $parts ),
            ] );
        }

        private function sync_webhooks_for_key( string $secretKey ) : array {
            $transferEvents = ['transfer.created', 'transfer.reversed', 'transfer.updated'];
            try {
                $stripe = new \Stripe\StripeClient($secretKey);
                $webhooks = $stripe->webhookEndpoints->all( [
                    'limit' => 20,
                ] );
                $synced = 0;
                foreach ( $webhooks->data as $endpoint ) {
                    $existing = $endpoint->enabled_events;
                    if ( in_array( '*', $existing ) ) {
                        continue;
                    }
                    $newEvents = $existing;
                    $needsUpdate = false;
                    foreach ( $transferEvents as $te ) {
                        if ( !in_array( $te, $existing ) ) {
                            $newEvents[] = $te;
                            $needsUpdate = true;
                        }
                    }
                    if ( $needsUpdate ) {
                        try {
                            $stripe->webhookEndpoints->update( $endpoint->id, [
                                'enabled_events' => array_values( array_unique( $newEvents ) ),
                            ] );
                            $synced++;
                        } catch ( \Stripe\Exception\PermissionException $e ) {
                            continue;
                        }
                    }
                }
                return [
                    'synced' => $synced,
                    'error'  => null,
                ];
            } catch ( \Stripe\Exception\PermissionException $e ) {
                return [
                    'synced' => 0,
                    'error'  => 'Restricted key — cannot manage webhooks',
                ];
            } catch ( \Throwable $e ) {
                return [
                    'synced' => 0,
                    'error'  => $e->getMessage(),
                ];
            }
        }

        public function sync_gateway_accounts() {
            check_ajax_referer( 'spp_sync_accounts', '_wpnonce' );
            if ( !current_user_can( 'manage_options' ) ) {
                wp_send_json_error( [
                    'message' => 'Insufficient permissions.',
                ] );
            }
            $gateway_id = sanitize_text_field( wp_unslash( $_POST['gateway_id'] ?? '' ) );
            if ( empty( $gateway_id ) ) {
                wp_send_json_error( [
                    'message' => 'No gateway specified.',
                ] );
            }
            $registry = \SplitPay\Core\IntegrationRegistry::getInstance();
            $gateway = null;
            foreach ( $registry->getActiveGateways() as $gw ) {
                if ( $gw->getId() === $gateway_id ) {
                    $gateway = $gw;
                    break;
                }
            }
            if ( !$gateway ) {
                wp_send_json_error( [
                    'message' => 'Gateway not found or not active.',
                ] );
            }
            $secretKey = ( method_exists( $gateway, 'getTransferSecretKey' ) ? $gateway->getTransferSecretKey() : '' );
            if ( empty( $secretKey ) ) {
                wp_send_json_error( [
                    'message' => 'Platform secret key not configured. Enter your Stripe secret key in the gateway settings on the Integrations tab.',
                ] );
            }
            require_once SPP_PLUGIN_DIR . '/includes/vendor/autoload.php';
            try {
                $stripe = new \Stripe\StripeClient($secretKey);
                $accts = $stripe->accounts->all( [
                    'limit' => 100,
                ] );
                global $wpdb;
                $table = $wpdb->prefix . SPP_STRP_ACCNT_TABLE;
                $synced = 0;
                // 3.7.5: every account returned by accounts->all() belongs to the
                // mode of the key that made the call (a test key only ever lists
                // test-mode accounts, a live key only live), so the key prefix is
                // the source of truth for this batch. Stamp it on both insert and
                // update — re-syncing also self-heals any stale/blank mode.
                $sync_mode = \SplitPay\Core\TransferLogger::resolveStripeMode( $secretKey );
                foreach ( $accts->data as $acct ) {
                    $existing = $wpdb->get_var( $wpdb->prepare( "SELECT bsd_sat_id FROM {$table} WHERE bsd_account_id = %s", $acct->id ) );
                    if ( $existing ) {
                        $wpdb->update( $table, [
                            'bsd_account_name'  => $acct->business_profile->name ?? $acct->settings->dashboard->display_name ?? '',
                            'bsd_account_email' => $acct->email ?? '',
                            'bsd_account_mode'  => $sync_mode,
                        ], [
                            'bsd_account_id' => $acct->id,
                        ] );
                    } else {
                        $wpdb->insert( $table, [
                            'bsd_account_id'    => $acct->id,
                            'bsd_account_name'  => $acct->business_profile->name ?? $acct->settings->dashboard->display_name ?? '',
                            'bsd_account_email' => $acct->email ?? '',
                            'bsd_account_mode'  => $sync_mode,
                        ] );
                    }
                    $synced++;
                }
                wp_send_json_success( [
                    'message' => sprintf( 'Synced %d connected account(s).', $synced ),
                ] );
            } catch ( \Throwable $e ) {
                wp_send_json_error( [
                    'message' => 'Stripe API error: ' . $e->getMessage(),
                ] );
            }
        }

        public function save_gateway_key() {
            check_ajax_referer( 'spp_save_gateway_key', '_wpnonce' );
            if ( !current_user_can( 'manage_options' ) ) {
                wp_send_json_error( [
                    'message' => 'Insufficient permissions.',
                ] );
            }
            $option = sanitize_text_field( wp_unslash( $_POST['option_name'] ?? '' ) );
            $key = sanitize_text_field( wp_unslash( $_POST['key_value'] ?? '' ) );
            $allowed = [
                'spp_wc_stripe_test_secret_key',
                'spp_wc_stripe_live_secret_key',
                'spp_pp_stripe_test_secret_key',
                'spp_pp_stripe_live_secret_key',
                'spp_fc_stripe_test_secret_key',
                'spp_fc_stripe_live_secret_key'
            ];
            if ( !in_array( $option, $allowed, true ) ) {
                wp_send_json_error( [
                    'message' => __( 'Invalid option.', 'bsd-split-pay-stripe-connect-woo' ),
                ] );
            }
            // 3.7.5 (sc-7194 follow-up — surfaced during E2E): the AJAX save path
            // uses its own validator separate from spp_sanitize_secret_key in
            // settings-api.php. Both must list the same four valid prefixes —
            // sk_test_, sk_live_, rk_test_, rk_live_ — or customers paste a
            // valid restricted-API test key (rk_test_) here, hit the AJAX path,
            // and get an error that doesn't even list the prefix they used.
            if ( empty( $key ) || strpos( $key, 'sk_' ) !== 0 && strpos( $key, 'rk_' ) !== 0 ) {
                wp_send_json_error( [
                    'message' => __( 'Invalid Secret Key format. Stripe secret keys start with "sk_test_", "sk_live_", "rk_test_", or "rk_live_".', 'bsd-split-pay-stripe-connect-woo' ),
                ] );
            }
            // 3.7.5: per-field MODE validation. The option name encodes the field's
            // mode (…_test_… vs …_live_…), so reject a live key pasted into a test
            // field (or vice-versa) — the live-key-in-a-test-slot misconfiguration
            // that makes Stripe reject every transfer ("No such destination: … a
            // similar object exists in test mode, but a live mode key was used").
            $field_mode = ( false !== strpos( $option, '_test_' ) ? 'test' : 'live' );
            $key_mode = '';
            if ( strpos( $key, 'sk_test_' ) === 0 || strpos( $key, 'rk_test_' ) === 0 ) {
                $key_mode = 'test';
            } elseif ( strpos( $key, 'sk_live_' ) === 0 || strpos( $key, 'rk_live_' ) === 0 ) {
                $key_mode = 'live';
            }
            if ( $key_mode !== '' && $key_mode !== $field_mode ) {
                wp_send_json_error( [
                    'message' => sprintf( 
                        /* translators: 1: the key's mode (TEST/LIVE), 2: the field's mode (TEST/LIVE) */
                        __( 'You entered a %1$s secret key in the %2$s field. It was not saved — put %1$s keys in the %1$s field, or paste your %2$s key here.', 'bsd-split-pay-stripe-connect-woo' ),
                        strtoupper( $key_mode ),
                        strtoupper( $field_mode )
                     ),
                ] );
            }
            update_option( $option, $key );
            /*
             * 3.7.5 (PT-7): When the merchant saves a NEW platform key, scrub the
             * legacy SPP-stored secret-key options + sync-state flags. This stops
             * stale keys (e.g. Peter @ tpforge.co.uk's `stripe_api_secret_key`
             * carrying an `acct_1TTKt2L…` key from an old install) from being
             * silently picked by the get_initial_accounts() resolver instead of
             * the brand-new key the merchant just pasted. Also wipes the
             * `bsd_strp_accnt` table so the next page load fetches connected
             * accounts FROM THE NEW KEY rather than reading stale rows.
             */
            $is_test_option = false !== strpos( $option, '_test_' );
            if ( $is_test_option ) {
                delete_option( 'stripe_test_api_secret_key' );
            } else {
                delete_option( 'stripe_api_secret_key' );
            }
            delete_option( 'is_all_sca_fetched' );
            delete_option( 'last_fetched_account' );
            delete_transient( 'spp_ifae_notice' );
            global $wpdb;
            // Table name is a hardcoded plugin constant ($wpdb->prefix . SPP_STRP_ACCNT_TABLE); no user input. TRUNCATE has no bindable parameters, so prepare() does not apply.
            $wpdb->query( 'TRUNCATE TABLE ' . $wpdb->prefix . SPP_STRP_ACCNT_TABLE );
            // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- constant identifier only, no bindable params.
            wp_send_json_success( [
                'message' => __( 'Key saved. Connected-accounts cache cleared — accounts will resync on next page load.', 'bsd-split-pay-stripe-connect-woo' ),
            ] );
        }

        public function stripe_key_notice_cb() {
            $woocommerce_stripe_settings = get_option( 'woocommerce_stripe_settings' );
            if ( isset( $woocommerce_stripe_settings['enabled'] ) && $woocommerce_stripe_settings['enabled'] == 'yes' ) {
                $test_mode = $woocommerce_stripe_settings['testmode'];
                if ( $test_mode == 'yes' ) {
                    $stripe_api_public_key = get_option( 'stripe_test_api_public_key', false );
                    $stripe_api_secret_key = get_option( 'stripe_test_api_secret_key', false );
                } else {
                    $stripe_api_public_key = get_option( 'stripe_api_public_key', false );
                    $stripe_api_secret_key = get_option( 'stripe_api_secret_key', false );
                }
                if ( empty( $stripe_api_public_key ) && empty( $stripe_api_secret_key ) ) {
                    ?>
						<div class="notice notice-warning">
							<p><?php 
                    echo wp_kses_post( __( '<b>Split Pay Plugin</b>', 'bsd-split-pay-stripe-connect-woo' ) );
                    ?></p>
							<p>
							<?php 
                    $tab_url = admin_url( 'admin.php?page=bsd-split-pay-stripe-connect-woo-settings&tab=stripe-configuration' );
                    /* translators: 1: opening anchor tag (with URL) to the Stripe configuration tab, 2: closing anchor tag, 3: opening anchor tag to the documentation, 4: closing anchor tag. */
                    echo wp_kses_post( sprintf(
                        __( ' In order for Transfers to work, please %1$sconfigure your Stripe API keys.%2$s Read our %3$sdocumentation%4$s for details.', 'bsd-split-pay-stripe-connect-woo' ),
                        '<a href="' . esc_url( $tab_url ) . '">',
                        '</a>',
                        '<a href="https://docs.splitpayplugin.com/getting-started/quick-start" target="_blank" rel="noopener noreferrer">',
                        '</a>'
                    ) );
                    ?></p>
						</div>
					<?php 
                }
            }
        }

        public function filter_masked_key( $value, $option, $old_value ) {
            if ( $option == 'stripe_test_api_public_key' || $option == 'stripe_test_api_secret_key' || $option == 'stripe_api_public_key' || $option == 'stripe_api_secret_key' ) {
                if ( strpos( $value, '******' ) !== false ) {
                    return $old_value;
                }
            }
            return $value;
        }

        public function spp_no_keys_redirect_rule() {
            if ( isset( $_GET['page'] ) && $_GET['page'] == 'bsd-split-pay-stripe-connect-woo-settings' && isset( $_GET['tab'] ) && $_GET['tab'] != 'stripe-configuration' ) {
                $woocommerce_stripe_settings = get_option( 'woocommerce_stripe_settings' );
                if ( isset( $woocommerce_stripe_settings['enabled'] ) && $woocommerce_stripe_settings['enabled'] == 'yes' ) {
                    $testmode = '';
                    if ( isset( $woocommerce_stripe_settings['testmode'] ) ) {
                        $testmode = $woocommerce_stripe_settings['testmode'];
                    }
                    $stripe_test_api_public_key = get_option( 'stripe_test_api_public_key', false );
                    $stripe_test_api_secret_key = get_option( 'stripe_test_api_secret_key', false );
                    $stripe_api_public_key = get_option( 'stripe_api_public_key', false );
                    $stripe_api_secret_key = get_option( 'stripe_api_secret_key', false );
                    if ( $testmode == 'yes' ) {
                        if ( empty( $stripe_test_api_public_key ) && empty( $stripe_test_api_secret_key ) ) {
                            wp_safe_redirect( admin_url( 'admin.php?page=bsd-split-pay-stripe-connect-woo-settings&tab=stripe-configuration' ) );
                            die;
                        }
                    } elseif ( empty( $stripe_api_public_key ) && empty( $stripe_api_secret_key ) ) {
                        wp_safe_redirect( admin_url( 'admin.php?page=bsd-split-pay-stripe-connect-woo-settings&tab=stripe-configuration' ) );
                        die;
                    }
                }
            }
        }

        public function spp_redirect_to_settings() {
            $spp_setting_redirect = get_option( 'spp_setting_redirect', false );
            if ( $spp_setting_redirect == '1' ) {
                delete_option( 'spp_setting_redirect' );
                wp_safe_redirect( admin_url( 'admin.php?page=bsd-split-pay-stripe-connect-woo-settings&tab=stripe-configuration' ) );
                die;
            }
            if ( $spp_setting_redirect == '2' ) {
                delete_option( 'spp_setting_redirect' );
                $woo_stripe_settings_url = admin_url( 'admin.php?page=bsd-split-pay-stripe-connect-woo-settings&tab=main' );
                wp_safe_redirect( $woo_stripe_settings_url );
                die;
            }
        }

        /**
         * WC email classes filter callback.
         * Transfer orde email class added in email list.
         */
        public function add_transfer_success_order_woocommerce_email( $email_classes ) {
            require SPP_PLUGIN_DIR . '/includes/admin/class-wc-transfer-order-email.php';
            // add the email class to the list of email classes that WooCommerce loads
            $email_classes['WC_Transfer_Order_Email'] = new WC_Transfer_Order_Email();
            return $email_classes;
        }

        /**
         * Change path for transfer email templates.
         */
        public function sps_get_woocommerce_template(
            $located,
            $template_name,
            $args,
            $template_path,
            $default_path
        ) {
            $plugin_dir_path = SPP_PLUGIN_DIR . '/wc-templates/';
            if ( file_exists( $plugin_dir_path . $template_name ) ) {
                if ( isset( $args['email']->id ) && $args['email']->id == 'wc_tarnsfer_order' ) {
                    $located = $plugin_dir_path . $template_name;
                    \BSD_Split_Pay_Stripe_Connect_Woo\spp_safe_wcs_log( ': sps_get_woocommerce_template-located-email ' . wp_json_encode( $located ) );
                }
                if ( isset( $args['email_obj']->id ) && $args['email_obj']->id == 'wc_tarnsfer_order' ) {
                    $located = $plugin_dir_path . $template_name;
                    \BSD_Split_Pay_Stripe_Connect_Woo\spp_safe_wcs_log( ': sps_get_woocommerce_template-located-email_obj ' . wp_json_encode( $located ) );
                }
            }
            return $located;
        }

        public function save_product_bulk_edit_data() {
            if ( !current_user_can( 'manage_woocommerce' ) ) {
                // wp_send_json() terminates — `die;` was unreachable.
                wp_send_json( array(
                    'success' => false,
                ) );
            }
            // Get the raw POST data
            $jsonInput = file_get_contents( 'php://input' );
            // Decode the JSON data
            $data = json_decode( $jsonInput, true );
            $get_security_key = count( $data ) - 1;
            $security = '';
            if ( isset( $data[$get_security_key]['security'] ) && !empty( $data[$get_security_key]['security'] ) ) {
                $security = $data[$get_security_key]['security'];
            }
            if ( wp_verify_nonce( $security, 'ajax-security' ) ) {
                if ( !empty( $data ) ) {
                    foreach ( $data as $dk ) {
                        $prod_id = $dk['product_id'];
                        $product_type = $dk['product_type'];
                        if ( isset( $dk['connected_account'] ) && !empty( $dk['connected_account'] ) ) {
                            $prod_valid_connected_account = array();
                            foreach ( $dk['connected_account'] as $dkk => $dvv ) {
                                $dk['transfer_type'][$dkk] = sanitize_text_field( $dk['transfer_type'][$dkk] );
                                $dk['transfer_type_percentage'][$dkk] = sanitize_text_field( $dk['transfer_type_percentage'][$dkk] );
                                $dk['transfer_type_amount'][$dkk] = sanitize_text_field( $dk['transfer_type_amount'][$dkk] );
                                /* Shipping */
                                $dk['shipping_transfer_type'][$dkk] = sanitize_text_field( $dk['shipping_transfer_type'][$dkk] );
                                $dk['shipping_transfer_percentage'][$dkk] = sanitize_text_field( $dk['shipping_transfer_percentage'][$dkk] );
                                $dk['shipping_transfer_amount'][$dkk] = sanitize_text_field( $dk['shipping_transfer_amount'][$dkk] );
                                $prod_valid_connected_account[$dkk] = sanitize_text_field( $dvv );
                            }
                            // 3.7.5 (data-loss guard): the bulk grid auto-submits EVERY
                            // rendered product. A vendor whose Stripe mode != the store's
                            // active mode is filtered out of its dropdown, so that select
                            // posts ''. Without this guard the update below would overwrite
                            // the saved mapping with [''] and silently wipe the vendor (the
                            // sc-7201 class). If the submitted vendor list is effectively
                            // empty but the product already has a saved vendor that is
                            // merely the WRONG MODE (filtered out, not user-cleared), skip
                            // this product. An intentional clear of an in-mode vendor still
                            // works: its mode matches the store, so this guard no-ops.
                            $spp_submitted_nonempty = array_filter( $prod_valid_connected_account, static function ( $v ) {
                                return trim( (string) $v ) !== '';
                            } );
                            if ( empty( $spp_submitted_nonempty ) ) {
                                $spp_existing_accts = get_post_meta( $prod_id, '_bsd_spscwt_product_connected_account', true );
                                if ( is_array( $spp_existing_accts ) && !empty( array_filter( $spp_existing_accts, static function ( $v ) {
                                    return trim( (string) $v ) !== '';
                                } ) ) ) {
                                    $spp_store_mode = ( class_exists( '\\SplitPay\\Core\\TransferLogger' ) ? \SplitPay\Core\TransferLogger::resolveStripeMode() : '' );
                                    $spp_ca_guard = new \SplitPay\Core\ConnectedAccounts();
                                    foreach ( $spp_existing_accts as $spp_existing_acct ) {
                                        $spp_existing_acct = trim( (string) $spp_existing_acct );
                                        if ( $spp_existing_acct === '' ) {
                                            continue;
                                        }
                                        $spp_existing_mode = $spp_ca_guard->getAccountMode( $spp_existing_acct );
                                        if ( $spp_existing_mode !== '' && $spp_store_mode !== '' && $spp_existing_mode !== $spp_store_mode ) {
                                            // Saved vendor is wrong-mode → the grid filtered it
                                            // out, the user didn't clear it → preserve it.
                                            continue 2;
                                        }
                                    }
                                }
                            }
                            if ( !empty( $prod_valid_connected_account ) ) {
                                update_post_meta( $prod_id, '_bsd_spscwt_product_connected_account', $prod_valid_connected_account );
                                update_post_meta( $prod_id, '_bsd_spscwt_product_type', $dk['transfer_type'] );
                                update_post_meta( $prod_id, '_stripe_connect_split_pay_transfer_percentage', $dk['transfer_type_percentage'] );
                                update_post_meta( $prod_id, '_bsd_spscwt_product_amount', $dk['transfer_type_amount'] );
                                update_post_meta( $prod_id, '_bsd_spscwt_shipping_type', $dk['shipping_transfer_type'] );
                                update_post_meta( $prod_id, '_bsd_prod_shipping_percentage', $dk['shipping_transfer_percentage'] );
                                if ( $product_type == 'simple' ) {
                                    update_post_meta( $prod_id, '_bsd_spscwt_shipping_amount', $dk['shipping_transfer_amount'] );
                                } elseif ( $product_type == 'variable' ) {
                                    update_post_meta( $prod_id, '_bsd_prod_shipping_amount', $dk['shipping_transfer_amount'] );
                                }
                                if ( $product_type === 'fc_product' ) {
                                    global $wpdb;
                                    $fc_meta_table = $wpdb->prefix . 'fct_product_meta';
                                    $fc_acct = reset( $prod_valid_connected_account );
                                    $fc_ttype = ( isset( $dk['transfer_type'][0] ) ? $dk['transfer_type'][0] : '' );
                                    $fc_pct = ( isset( $dk['transfer_type_percentage'][0] ) ? $dk['transfer_type_percentage'][0] : '' );
                                    $fc_amt = ( isset( $dk['transfer_type_amount'][0] ) ? $dk['transfer_type_amount'][0] : '' );
                                    $fc_stype = ( isset( $dk['shipping_transfer_type'][0] ) ? $dk['shipping_transfer_type'][0] : '' );
                                    $fc_spct = ( isset( $dk['shipping_transfer_percentage'][0] ) ? $dk['shipping_transfer_percentage'][0] : '' );
                                    $fc_samt = ( isset( $dk['shipping_transfer_amount'][0] ) ? $dk['shipping_transfer_amount'][0] : '' );
                                    $fc_json = wp_json_encode( [
                                        'name'                         => 'Split Pay Transfer',
                                        'connected_account'            => $fc_acct,
                                        'transfer_type'                => $fc_ttype,
                                        'transfer_percentage'          => $fc_pct,
                                        'transfer_amount'              => $fc_amt,
                                        'shipping_transfer_type'       => ( $fc_stype ?: 'none' ),
                                        'shipping_transfer_percentage' => $fc_spct,
                                        'shipping_transfer_amount'     => $fc_samt,
                                        'enabled'                      => true,
                                        'conditionals'                 => [],
                                    ] );
                                    $exists = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM {$fc_meta_table} WHERE object_id = %d AND meta_key = 'split_pay_connect' LIMIT 1", $prod_id ) );
                                    if ( $exists ) {
                                        $wpdb->update( $fc_meta_table, [
                                            'meta_value' => $fc_json,
                                            'updated_at' => current_time( 'mysql' ),
                                        ], [
                                            'id' => $exists,
                                        ] );
                                    } else {
                                        $wpdb->insert( $fc_meta_table, [
                                            'object_id'   => $prod_id,
                                            'object_type' => 'product_integration',
                                            'meta_key'    => 'split_pay_connect',
                                            'meta_value'  => $fc_json,
                                            'created_at'  => current_time( 'mysql' ),
                                            'updated_at'  => current_time( 'mysql' ),
                                        ] );
                                    }
                                }
                            }
                        }
                    }
                }
            }
            // wp_send_json() terminates — `die;` was unreachable.
            wp_send_json( array(
                'success' => true,
            ) );
        }

        public function add_more_bulk_editor_filter() {
            if ( !current_user_can( 'manage_woocommerce' ) ) {
                // wp_send_json_error() terminates — `die;` was unreachable.
                wp_send_json_error( 'Unauthorized' );
            }
            $data_id = ( isset( $_POST['count'] ) ? absint( wp_unslash( $_POST['count'] ) ) + 1 : 1 );
            $args = array(
                'taxonomy'   => 'product_cat',
                'orderby'    => 'name',
                'order'      => 'ASC',
                'hide_empty' => false,
            );
            $product_categories = get_terms( $args );
            $html = '';
            ob_start();
            ?>
			<div class="find-product-list__row more-<?php 
            echo (int) $data_id;
            ?>">
				<div class="row">
					<div class="find-product-list__col find-product-list__col_1">
						<select name="filter_name[]" class="pwbe-filter-field pwbe-filter-name filter-name" data-id="<?php 
            echo (int) $data_id;
            ?>">
							<option value="categories" data-type="categories"><?php 
            echo esc_html__( 'Category', 'bsd-split-pay-stripe-connect-woo' );
            ?></option>
							<option value="post_title" data-type="string"><?php 
            echo esc_html__( 'Product name', 'bsd-split-pay-stripe-connect-woo' );
            ?></option>
							<option value="sku" data-type="string"><?php 
            echo esc_html__( 'SKU', 'bsd-split-pay-stripe-connect-woo' );
            ?></option>
							<option value="pa" data-type="string"><?php 
            echo esc_html__( 'Product Attribute', 'bsd-split-pay-stripe-connect-woo' );
            ?></option>
						</select>
					</div>

					<div class="find-product-list__col find-product-list__col_2 category-fields category-fields-<?php 
            echo (int) $data_id;
            ?>">
						<select name="filter_cat_type[]" class="pwbe-filter-field pwbe-filter-type filter-cat-type" data-id="<?php 
            echo (int) $data_id;
            ?>">
							<option value="is_any_of"><?php 
            echo esc_html__( 'is any of', 'bsd-split-pay-stripe-connect-woo' );
            ?></option>
							<option value="is_none_of"><?php 
            echo esc_html__( 'is none of', 'bsd-split-pay-stripe-connect-woo' );
            ?></option>
						</select>
					</div>

					<div class="find-product-list__col find-product-list__col_2 product-fields product-fields-<?php 
            echo (int) $data_id;
            ?>">
						<select name="filter_product_type[]" class="pwbe-filter-field pwbe-filter-type filter-product-type" data-id="<?php 
            echo (int) $data_id;
            ?>">
							<option value="contains"><?php 
            echo esc_html__( 'contains', 'bsd-split-pay-stripe-connect-woo' );
            ?></option>
							<option value="does_not_contain"><?php 
            echo esc_html__( 'does not contain', 'bsd-split-pay-stripe-connect-woo' );
            ?></option>
							<option value="is"><?php 
            echo esc_html__( 'is', 'bsd-split-pay-stripe-connect-woo' );
            ?></option>
							<option value="is_not"><?php 
            echo esc_html__( 'is not', 'bsd-split-pay-stripe-connect-woo' );
            ?></option>
							<option value="begins_with"><?php 
            echo esc_html__( 'begins with', 'bsd-split-pay-stripe-connect-woo' );
            ?></option>
							<option value="ends_with"><?php 
            echo esc_html__( 'ends with', 'bsd-split-pay-stripe-connect-woo' );
            ?></option>
						</select>
					</div>

					<div class="find-product-list__col find-product-list__col_2 product-fields product-fields-<?php 
            echo (int) $data_id;
            ?>">
						<input type="text" name="search_text[]" value="" data-id="<?php 
            echo (int) $data_id;
            ?>" size="40" class="pro-filter-input" />
					</div>

					<?php 
            if ( !empty( $product_categories ) && !is_wp_error( $product_categories ) ) {
                ?>
						<div class="find-product-list__col find-product-list__col_3 category-fields category-fields-<?php 
                echo (int) $data_id;
                ?>">
							<select name="filter_categories[]" multiple="true" class="pwbe-filter-field pwbe-filter-name pwbe-filter-select filter-categories" data-id="<?php 
                echo (int) $data_id;
                ?>">
								<?php 
                foreach ( $product_categories as $terms ) {
                    ?>
									<option value="<?php 
                    echo (int) $terms->term_id;
                    ?>"><?php 
                    echo esc_html( $terms->name );
                    ?></option>
								<?php 
                }
                ?>
							</select>
						</div>
					<?php 
            }
            ?>
					<div class="find-product-list__btns-pls-mns">
						<button class="find-plus add_more" data-id="<?php 
            echo (int) $data_id;
            ?>">+</button>
						<button class="find-minus remove_more" data-id="<?php 
            echo (int) $data_id;
            ?>">-</button>
					</div>
					<div class="find-product-list__btns">
						<button class="btn-link add_more" data-id="<?php 
            echo (int) $data_id;
            ?>">
							+ <?php 
            echo esc_html__( 'Add a filter', 'bsd-split-pay-stripe-connect-woo' );
            ?></button>
						<button class="btn-link remove_more" data-id="<?php 
            echo (int) $data_id;
            ?>">
							- <?php 
            echo esc_html__( 'Remove', 'bsd-split-pay-stripe-connect-woo' );
            ?></button>
					</div>
				</div>
			</div>
			<?php 
            $html .= ob_get_contents();
            ob_end_clean();
            // wp_send_json_success() terminates — `die();` was unreachable.
            wp_send_json_success( $html );
        }

        public function product_search_data() {
            if ( !current_user_can( 'manage_woocommerce' ) ) {
                // wp_send_json_error() terminates — `die;` was unreachable.
                wp_send_json_error( 'Unauthorized' );
            }
            if ( isset( $_POST['security'] ) && wp_verify_nonce( wp_unslash( $_POST['security'] ), 'ajax-security' ) ) {
                require_once SPP_PLUGIN_DIR . '/includes/admin/partials/spp-product-serach-data-html.php';
            }
        }

        public function get_product_title_sku_result( $pro_query = array(), $get_tax_products_id = array(), $all_variation = true ) {
            global $wpdb;
            $common_where = '';
            $products_ids = $results = array();
            if ( !empty( $pro_query ) ) {
                foreach ( $pro_query as $key => $value ) {
                    if ( $value['pa'] ) {
                        continue;
                    }
                    // audit P1.4 — default before the conditional branches that
                    // may not assign $field_name. The inner switch reads
                    // $field_name unconditionally.
                    $field_name = 'p.post_name';
                    if ( $value['title'] ) {
                        $field_name = 'p.post_name';
                    }
                    if ( $value['sku'] ) {
                        $field_name = 'pm.meta_key = "_sku" AND pm.meta_value';
                    }
                    foreach ( $value as $key2 => $data ) {
                        $value = $data['value'];
                        switch ( $data['condition'] ) {
                            case 'is':
                                $common_where .= " AND {$field_name} = '" . esc_sql( $value ) . "'";
                                break;
                            case 'is_not':
                                $common_where .= " AND {$field_name} != '" . esc_sql( $value ) . "'";
                                break;
                            case 'contains':
                                $common_where .= " AND {$field_name} LIKE '%" . sanitize_title( $value ) . "%'";
                                break;
                            case 'does_not_contain':
                                $common_where .= " AND {$field_name} NOT LIKE '%" . sanitize_title( $value ) . "%'";
                                break;
                            case 'begins_with':
                                $common_where .= " AND {$field_name} LIKE '" . sanitize_title( $value ) . "%'";
                                break;
                            case 'ends_with':
                                $common_where .= " AND {$field_name} LIKE '%" . sanitize_title( $value ) . "'";
                                break;
                        }
                    }
                }
            }
            $query = "SELECT DISTINCT  p.ID, p.post_parent  FROM {$wpdb->posts} p ";
            /*
            			if ( ! empty( $categories ) ) {
            				$query .= "LEFT JOIN {$wpdb->term_relationships} tr ON (p.ID = tr.object_id)";
            			}
            
            			if ( ! empty( $categories ) ) {
            				foreach ( $categories as $key => $cat ) {
            					$query .= "AND ( ( tr.term_taxonomy_id ".$cat["condition"]." (".implode(",", $cat["value"]).") ) ) ";
            				}
            			} */
            $query .= " JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id WHERE 1=1 ";
            $tax_query = '';
            if ( !empty( $get_tax_products_id ) ) {
                $tax_query = " AND ( \n\t\t\t\t\t\t( \n\t\t\t\t\t\t\tp.ID NOT IN (\n\t\t\t\t\t\t\t\t\t\tSELECT object_id\n\t\t\t\t\t\t\t\t\t\tFROM {$wpdb->term_relationships} tr\n\t\t\t\t\t\t\t\t\t\tWHERE tr.term_taxonomy_id IN (" . implode( ',', array_map( 'absint', $get_tax_products_id ) ) . ')
									)
						)
					)';
            }
            $query .= " AND (p.post_type = 'product' OR p.post_type = 'product_variation') AND p.post_status = 'publish' {$common_where} {$tax_query}";
            // $query is built from core table refs ($wpdb->posts/postmeta) plus values already sanitized inline (absint on IDs, sanitize_title on attribute values); there are no remaining unescaped user inputs to bind.
            $products_ids = $wpdb->get_results( $query, ARRAY_A );
            // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- values pre-sanitized with absint()/sanitize_title() before concatenation.
            foreach ( $products_ids as $id ) {
                if ( $all_variation ) {
                    $results[] = $id['ID'];
                    $results[] = $id['post_parent'];
                } else {
                    $results[$id['post_parent']][] = $id['ID'];
                }
            }
            if ( $all_variation && !empty( $results ) ) {
                $results = array_unique( $results );
            }
            return $results;
        }

        public function get_product_from_attribute_search( $pro_query = array(), $all_variation = true ) {
            global $wpdb;
            $common_where = '';
            $products_ids = $results = array();
            if ( !empty( $pro_query ) ) {
                foreach ( $pro_query as $key => $value ) {
                    $field_name = 'pm.meta_key like "%attribute_pa_%" AND pm.meta_value';
                    foreach ( $value as $key2 => $data ) {
                        $value = $data['value'];
                        switch ( $data['condition'] ) {
                            case 'is':
                                $common_where .= " AND {$field_name} = '" . sanitize_title( $value ) . "' ";
                                break;
                            case 'is_not':
                                $common_where .= " AND {$field_name} != '" . sanitize_title( $value ) . "'";
                                break;
                            case 'contains':
                                $common_where .= " AND {$field_name} LIKE '%" . sanitize_title( $value ) . "%'";
                                break;
                            case 'does_not_contain':
                                $common_where .= " AND {$field_name} NOT LIKE '%" . sanitize_title( $value ) . "%'";
                                break;
                            case 'begins_with':
                                $common_where .= " AND {$field_name} LIKE '" . sanitize_title( $value ) . "%'";
                                break;
                            case 'ends_with':
                                $common_where .= " AND {$field_name} LIKE '%" . sanitize_title( $value ) . "'";
                                break;
                        }
                    }
                }
            }
            $query = "SELECT p.ID, p.post_parent  FROM {$wpdb->posts} p\n            JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id\n            WHERE (p.post_type = 'product' OR p.post_type = 'product_variation') AND p.post_status = 'publish' {$common_where} ";
            // $query uses core table refs ($wpdb->posts/postmeta); the only interpolated values come from sanitize_title() above, so there are no unescaped user inputs to bind.
            $products_ids = $wpdb->get_results( $query, ARRAY_A );
            // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- values pre-sanitized with sanitize_title() before concatenation.
            foreach ( $products_ids as $id ) {
                if ( $all_variation ) {
                    $results[] = $id['ID'];
                    $results[] = $id['post_parent'];
                } else {
                    $results[$id['post_parent']][] = $id['ID'];
                }
            }
            if ( $all_variation && !empty( $results ) ) {
                $results = array_unique( $results );
            }
            return $results;
        }

        public function get_taxonomy_product_ids( $cat = array() ) {
            global $wpdb;
            $products_ids = $results = array();
            if ( $cat['condition'] == 'IN' ) {
                $query = "SELECT p.ID FROM {$wpdb->posts} p LEFT JOIN {$wpdb->term_relationships} tr ON (p.ID = tr.object_id) WHERE 1=1 AND (  tr.term_taxonomy_id IN (" . implode( ',', array_map( 'absint', $cat['value'] ) ) . ") )  AND p.post_type = 'product' AND p.post_status = 'publish' GROUP BY p.ID ORDER BY p.ID DESC";
            } else {
                $query = "SELECT p.ID FROM {$wpdb->posts} p LEFT JOIN {$wpdb->term_relationships} tr ON (p.ID = tr.object_id) WHERE 1=1 AND ( \n\t\t\t\t\t\t( \n\t\t\t\t\t\t\tp.ID NOT IN (\n\t\t\t\t\t\t\t\t\t\tSELECT object_id\n\t\t\t\t\t\t\t\t\t\tFROM {$wpdb->term_relationships} tr\n\t\t\t\t\t\t\t\t\t\tWHERE tr.term_taxonomy_id IN (" . implode( ',', array_map( 'absint', $cat['value'] ) ) . ")\n\t\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t)\n\t\t\t\t\t)  AND p.post_type = 'product' AND p.post_status = 'publish' GROUP BY p.ID ORDER BY p.ID DESC";
            }
            // $query uses core table refs ($wpdb->posts/term_relationships); the only interpolated values are term IDs mapped through absint(), so there are no unescaped user inputs to bind.
            $products_ids = $wpdb->get_results( $query, ARRAY_A );
            // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- term IDs pre-sanitized with absint() before concatenation.
            foreach ( $products_ids as $id ) {
                $results[] = $id['ID'];
            }
            if ( !empty( $results ) ) {
                $results = array_unique( $results );
            }
            return $results;
        }

        public function sps_admin_notices() {
            if ( isset( $_GET['msg'] ) && $_GET['msg'] == 'ob_failed' ) {
                $stored_ob_failed = get_transient( 'ob_failed' );
                if ( !empty( $stored_ob_failed ) ) {
                    ?>
					<div class="notice notice-warning is-dismissible">
						<p><?php 
                    echo esc_html( $stored_ob_failed );
                    ?> </p>
					</div>
					<?php 
                } else {
                    ?>
					<div class="notice notice-warning is-dismissible">
						<p>Account failed to connect. Please contact the site administrator for details. </p>
					</div>
					<?php 
                }
            }
            if ( isset( $_GET['msg'] ) && $_GET['msg'] == 'ob_success' ) {
                ?>
				<div class="notice notice-success is-dismissible">
					<p>Account successfully connected. </p>
				</div>
				<?php 
            }
            if ( current_user_can( 'manage_woocommerce' ) ) {
                $this->spp_configuration_incomplete_notice();
            }
        }

        private function spp_configuration_incomplete_notice() {
            $screen = get_current_screen();
            if ( !$screen || strpos( $screen->id, 'bsd-split-pay' ) !== false ) {
                return;
            }
            $issues = array();
            $woocommerce_stripe_settings = get_option( 'woocommerce_stripe_settings' );
            $test_mode = isset( $woocommerce_stripe_settings['testmode'] ) && $woocommerce_stripe_settings['testmode'] === 'yes';
            if ( $test_mode ) {
                if ( empty( get_option( 'stripe_test_api_secret_key', '' ) ) ) {
                    $issues[] = __( 'Stripe Test Secret Key is not configured.', 'bsd-split-pay-stripe-connect-woo' );
                }
            } else {
                if ( empty( get_option( 'stripe_api_secret_key', '' ) ) ) {
                    $issues[] = __( 'Stripe Live Secret Key is not configured.', 'bsd-split-pay-stripe-connect-woo' );
                }
            }
            $sps_webhook_update = get_option( 'sps_webhook_update', '0' );
            $sps_test_webhook_update = get_option( 'sps_test_webhook_update', '0' );
            if ( $test_mode && $sps_test_webhook_update !== '1' ) {
                $issues[] = __( 'Test Mode webhook is not synced.', 'bsd-split-pay-stripe-connect-woo' );
            } elseif ( !$test_mode && $sps_webhook_update !== '1' ) {
                $issues[] = __( 'Live Mode webhook is not synced.', 'bsd-split-pay-stripe-connect-woo' );
            }
            if ( !empty( $issues ) ) {
                $settings_url = admin_url( 'admin.php?page=bsd-split-pay-stripe-connect-woo-settings&tab=stripe-configuration' );
                ?>
				<div class="notice notice-warning is-dismissible">
					<p>
						<strong><?php 
                esc_html_e( 'Split Pay Plugin:', 'bsd-split-pay-stripe-connect-woo' );
                ?></strong>
						<?php 
                esc_html_e( 'Your configuration is incomplete. Transfers will not work until the following are resolved:', 'bsd-split-pay-stripe-connect-woo' );
                ?>
					</p>
					<ul style="list-style: disc; margin-left: 20px;">
						<?php 
                foreach ( $issues as $issue ) {
                    ?>
							<li><?php 
                    echo esc_html( $issue );
                    ?></li>
						<?php 
                }
                ?>
					</ul>
					<p><a href="<?php 
                echo esc_url( $settings_url );
                ?>" class="button button-small"><?php 
                esc_html_e( 'Go to Split Pay Settings', 'bsd-split-pay-stripe-connect-woo' );
                ?></a></p>
				</div>
				<?php 
            }
        }

        public function sps_custom_menus() {
            if ( current_user_can( 'spp_custom_menu' ) && !current_user_can( 'manage_woocommerce' ) ) {
                add_menu_page(
                    __( 'Stripe Connect', 'bsd-split-pay-stripe-connect-woo' ),
                    __( 'Stripe Connect', 'bsd-split-pay-stripe-connect-woo' ),
                    'spp_custom_menu',
                    'split-pay-stripe-connect',
                    array($this, 'sps_connect_menu_page_cb'),
                    'data:image/svg+xml;base64,' . base64_encode( '<svg xmlns="http://www.w3.org/2000/svg" height="10" width="10" viewBox="0 0 384 512"><path fill="#ffffff" d="M155.3 154.6c0-22.3 18.6-30.9 48.4-30.9 43.4 0 98.5 13.3 141.9 36.7V26.1C298.3 7.2 251.1 0 203.8 0 88.1 0 11 60.4 11 161.4c0 157.9 216.8 132.3 216.8 200.4 0 26.4-22.9 34.9-54.7 34.9-47.2 0-108.2-19.5-156.1-45.5v128.5a396.1 396.1 0 0 0 156 32.4c118.6 0 200.3-51 200.3-153.6 0-170.2-218-139.7-218-203.9z"/></svg>' )
                );
                remove_menu_page( 'index.php' );
                remove_menu_page( 'profile.php' );
            }
        }

        public function sps_connect_menu_page_cb() {
            require_once SPP_PLUGIN_DIR . '/includes/admin/partials/sps-connect-menu-page-html.php';
        }

        public function sps_woocommerce_disable_admin_bar( $show ) {
            if ( current_user_can( 'spp_custom_menu' ) && !current_user_can( 'manage_woocommerce' ) ) {
                $show = false;
            }
            return $show;
        }

        public function sps_woocommerce_login_redirect( $redirect, $user ) {
            $dashboard = admin_url( '/admin.php?page=split-pay-stripe-connect' );
            if ( $user->has_cap( 'spp_custom_menu' ) && !$user->has_cap( 'manage_woocommerce' ) ) {
                $redirect = $dashboard;
            }
            return $redirect;
        }

        public function sps_login_redirect( $redirect, $requested_redirect_to, $user ) {
            if ( !is_wp_error( $user ) ) {
                $dashboard = admin_url( '/admin.php?page=split-pay-stripe-connect' );
                if ( $user->has_cap( 'spp_custom_menu' ) && !$user->has_cap( 'manage_woocommerce' ) ) {
                    $redirect = $dashboard;
                }
            }
            return $redirect;
        }

        public function sps_add_roles() {
            $capabilities = array(
                'read'            => true,
                'level_0'         => true,
                'spp_custom_menu' => true,
            );
            add_role( 'vendor_split_pay_plugin', esc_html__( 'Vendor (Split Pay Plugin)', 'bsd-split-pay-stripe-connect-woo' ), $capabilities );
        }

        public function sps_register_form_fields() {
            ?>
			<p class="rvspp_wrapper">
				<input name="register_vendor_split_pay_plugin" type="checkbox" id="register_vendor_split_pay_plugin" value="1">
				<label for="register_vendor_split_pay_plugin"><?php 
            esc_html_e( 'Register as a Vendor', 'bsd-split-pay-stripe-connect-woo' );
            ?></label>
			</p>
			<style>
				.rvspp_wrapper {
					width: 100%;
				}
			</style>
			<?php 
        }

        public function sps_save_register_fields( $user_id ) {
            if ( isset( $_POST['register_vendor_split_pay_plugin'] ) ) {
                update_user_meta( $user_id, '_register_vendor_split_pay_plugin', sanitize_text_field( wp_unslash( $_POST['register_vendor_split_pay_plugin'] ) ) );
                $u = new \WP_User($user_id);
                // Remove role
                $u->remove_role( 'subscriber' );
                // Add role
                $u->add_role( 'vendor_split_pay_plugin' );
            }
        }

        /**
         * Resolve the best available display name from a Stripe Account object.
         * Falls through: business_profile.name → settings.dashboard.display_name
         *   → company.name → individual first+last → account ID.
         */
        private function resolve_account_display_name( $acc ) {
            if ( !empty( $acc->business_profile->name ) ) {
                return $acc->business_profile->name;
            }
            if ( isset( $acc->settings->dashboard->display_name ) && !empty( $acc->settings->dashboard->display_name ) ) {
                return $acc->settings->dashboard->display_name;
            }
            if ( isset( $acc->company->name ) && !empty( $acc->company->name ) ) {
                return $acc->company->name;
            }
            if ( isset( $acc->individual ) ) {
                $first = ( isset( $acc->individual->first_name ) ? $acc->individual->first_name : '' );
                $last = ( isset( $acc->individual->last_name ) ? $acc->individual->last_name : '' );
                $full = trim( $first . ' ' . $last );
                if ( !empty( $full ) ) {
                    return $full;
                }
            }
            return '';
        }

        /**
         * Update an existing account row if the stored name/email can be improved.
         * Returns true if the row was updated, false otherwise.
         */
        private function maybe_update_account_row(
            $row,
            $account_id,
            $account_name,
            $account_email
        ) {
            global $wpdb;
            $update_data = array();
            $update_where = array(
                'bsd_account_id' => $account_id,
            );
            $stored_name = ( isset( $row['bsd_account_name'] ) ? $row['bsd_account_name'] : '' );
            $stored_name_is_bad = empty( $stored_name ) || $stored_name === $account_id;
            if ( $stored_name_is_bad && !empty( $account_name ) ) {
                $update_data['bsd_account_name'] = $account_name;
            } elseif ( $stored_name === $account_id && empty( $account_name ) ) {
                $update_data['bsd_account_name'] = '';
            }
            if ( empty( $row['bsd_account_email'] ) && !empty( $account_email ) ) {
                $update_data['bsd_account_email'] = $account_email;
            }
            if ( !empty( $update_data ) ) {
                $wpdb->update( $wpdb->prefix . SPP_STRP_ACCNT_TABLE, $update_data, $update_where );
                return true;
            }
            return false;
        }

        private function stp_retrieve_keys() {
            // Get settings from WooCommerce Stripe Gateway plugin
            $data = null;
            $woocommerce_stripe_settings = get_option( 'woocommerce_stripe_settings' );
            if ( !$woocommerce_stripe_settings ) {
                return false;
            }
            $wc_stripe_test_mode = ( isset( $woocommerce_stripe_settings['testmode'] ) ? $woocommerce_stripe_settings['testmode'] : null );
            if ( $wc_stripe_test_mode == 'yes' ) {
                $stripe_test_api_secret_key = get_option( 'stripe_test_api_secret_key', false );
                $stripe_key = $stripe_test_api_secret_key;
            } else {
                $stripe_api_secret_key = get_option( 'stripe_api_secret_key', false );
                $stripe_key = $stripe_api_secret_key;
            }
            // Get our settings and theirs
            $data = array(
                'connectedKey'      => get_option( 'bsd_split_pay_stripe_connect_woo_stripe_connected_account' ),
                'bsd_spscwt_type'   => get_option( 'bsd_spscwt_type' ),
                'percentage'        => get_option( 'bsd_split_pay_stripe_connect_woo_transfer_percentage' ),
                'bsd_spscwt_amount' => get_option( 'bsd_spscwt_amount' ),
                'secret_key'        => $stripe_key,
            );
            return $data;
        }

        public function onboarding_account_cb() {
            global $wpdb;
            $current_user = wp_get_current_user();
            $data_keys = $this->stp_retrieve_keys();
            $stripe_key = $data_keys['secret_key'];
            if ( isset( $_GET['action'] ) && $_GET['action'] == 'onboarding' ) {
                try {
                    // $random_email = $this->get_random_email();
                    // 3.7.5 static-analysis: wp_get_current_user() always returns
                    // WP_User; ID > 0 guards anonymous visitors.
                    if ( $current_user->ID > 0 ) {
                        $stripe = new \Stripe\StripeClient($stripe_key);
                        /**
                         * Filter the arguments passed to Stripe's accounts->create() when
                         * an admin clicks the "Onboard a new connected account" button.
                         *
                         * See VendorConnectShortcode.php for the rationale (sc-7186) and
                         * an example of returning Accounts v2 controller-property args.
                         *
                         * @since 3.7.5
                         *
                         * @param array  $args    Args passed to $stripe->accounts->create().
                         * @param string $context 'admin' (this site) or 'shortcode' (front-end).
                         *
                         * @link https://docs.stripe.com/connect/accounts-v2
                         * @todo SPP 3.8.0 (sc-7190) — full v1 → v2 migration.
                         */
                        $account_create_args = apply_filters( 'spp_account_create_args', array(
                            'type' => 'standard',
                        ), 'admin' );
                        $account_create_response = $stripe->accounts->create( $account_create_args );
                        if ( !empty( $account_create_response ) && isset( $account_create_response->id ) && !empty( $account_create_response->id ) ) {
                            $account_link_obj = $stripe->accountLinks->create( array(
                                'account'     => $account_create_response->id,
                                'refresh_url' => admin_url( '/admin.php?page=split-pay-stripe-connect&action=onboard_failure&acc_id=' . $account_create_response->id ),
                                'return_url'  => admin_url( '/admin.php?page=split-pay-stripe-connect&action=onboard_success&acc_id=' . $account_create_response->id ),
                                'type'        => 'account_onboarding',
                            ) );
                            if ( !empty( $account_link_obj ) && isset( $account_link_obj['url'] ) && !empty( $account_link_obj['url'] ) ) {
                                // Whitelist Stripe-hosted onboarding hosts so wp_safe_redirect()
                                // doesn't reject them.
                                $spp_stripe_hosts = function ( $hosts ) {
                                    $hosts[] = 'connect.stripe.com';
                                    $hosts[] = 'hooks.stripe.com';
                                    $hosts[] = 'dashboard.stripe.com';
                                    return $hosts;
                                };
                                add_filter( 'allowed_redirect_hosts', $spp_stripe_hosts );
                                wp_safe_redirect( esc_url_raw( $account_link_obj['url'] ) );
                                remove_filter( 'allowed_redirect_hosts', $spp_stripe_hosts );
                                die;
                            }
                        }
                    }
                } catch ( \Exception $e ) {
                    $error_message = $e->getMessage();
                    \BSD_Split_Pay_Stripe_Connect_Woo\spp_safe_wcs_log( $this->log_prefix . ': error_message ' . wp_json_encode( $error_message ) );
                    set_transient( 'ob_failed', $error_message, 120 );
                    wp_safe_redirect( admin_url( '/admin.php?page=split-pay-stripe-connect&msg=ob_failed' ) );
                    exit;
                }
            } elseif ( isset( $_GET['action'] ) && $_GET['action'] == 'onboard_success' ) {
                if ( isset( $_GET['acc_id'] ) && !empty( $_GET['acc_id'] ) ) {
                    $onboard_acc_id = sanitize_text_field( wp_unslash( $_GET['acc_id'] ) );
                    try {
                        $stripe = new \Stripe\StripeClient($stripe_key);
                        $account_obj = $stripe->accounts->retrieve( $onboard_acc_id, array() );
                        if ( isset( $account_obj->tos_acceptance->date ) && !empty( $account_obj->tos_acceptance->date ) ) {
                            $account_id = $onboard_acc_id;
                            if ( isset( $account_obj->business_profile->name ) && !empty( $account_obj->business_profile->name ) ) {
                                $account_name = $account_obj->business_profile->name;
                            } else {
                                $account_name = $account_id;
                            }
                            // 3.7.5: stamp the Stripe mode this account was onboarded
                            // in (derived from the key prefix of the secret key that
                            // created it) so it only appears in the matching mode's
                            // dropdowns and can't be paid with a wrong-mode key.
                            $onboard_mode = \SplitPay\Core\TransferLogger::resolveStripeMode( $stripe_key );
                            $is_added = $wpdb->query( 
                                // Values are bound via prepare() placeholders; only the constant table name ($wpdb->prefix . SPP_STRP_ACCNT_TABLE) is concatenated, which prepare() cannot parameterize.
                                $wpdb->prepare( 
                                    'insert into ' . $wpdb->prefix . SPP_STRP_ACCNT_TABLE . ' (bsd_account_id, bsd_account_name, bsd_account_mode) values (%s, %s, %s)',
                                    // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- constant table identifier; data is bound via %s placeholders.
                                    array($account_id, $account_name, $onboard_mode)
                                 )
                             );
                            update_user_meta( $current_user->ID, 'account_id', $account_id );
                            update_user_meta( $current_user->ID, 'is_onboard', '1' );
                            wp_safe_redirect( admin_url( '/admin.php?page=split-pay-stripe-connect&msg=ob_success' ) );
                            exit;
                        }
                    } catch ( \Exception $e ) {
                        $error_message = $e->getMessage();
                        \BSD_Split_Pay_Stripe_Connect_Woo\spp_safe_wcs_log( $this->log_prefix . ': error_message ' . wp_json_encode( $error_message ) );
                        set_transient( 'ob_failed', $error_message, 120 );
                        wp_safe_redirect( admin_url( '/admin.php?page=split-pay-stripe-connect&msg=ob_failed' ) );
                        exit;
                    }
                }
            } elseif ( isset( $_GET['action'] ) && $_GET['action'] == 'onboard_failure' ) {
                if ( isset( $_GET['acc_id'] ) && !empty( $_GET['acc_id'] ) ) {
                    $failed_acc_id = sanitize_text_field( wp_unslash( $_GET['acc_id'] ) );
                    $stripe = new \Stripe\StripeClient($stripe_key);
                    $account_obj = $stripe->accounts->retrieve( $failed_acc_id, array() );
                    if ( !empty( $account_obj ) ) {
                        try {
                            $stripe->accounts->delete( $failed_acc_id, array() );
                            update_user_meta( $current_user->ID, 'failed_account_id', $failed_acc_id );
                            // update_user_meta($current_user->ID, "is_onboard", "0");
                            wp_safe_redirect( admin_url( '/admin.php?page=split-pay-stripe-connect&msg=ob_failed' ) );
                            exit;
                        } catch ( \Exception $e ) {
                            $error = $e->getError();
                            $error_type = $error->type;
                            $error_message = $error->message;
                            $error_request_log_url = $error->request_log_url;
                            \BSD_Split_Pay_Stripe_Connect_Woo\spp_safe_wcs_log( $this->log_prefix . ': error_type ' . wp_json_encode( $error_type ) );
                            \BSD_Split_Pay_Stripe_Connect_Woo\spp_safe_wcs_log( $this->log_prefix . ': error_message ' . wp_json_encode( $error_message ) );
                            \BSD_Split_Pay_Stripe_Connect_Woo\spp_safe_wcs_log( $this->log_prefix . ': error_request_log_url ' . wp_json_encode( $error_request_log_url ) );
                            set_transient( 'ob_failed', $error_message, 120 );
                            wp_safe_redirect( admin_url( '/admin.php?page=split-pay-stripe-connect&msg=ob_failed' ) );
                            exit;
                        }
                    }
                }
            } elseif ( isset( $_GET['action'] ) && $_GET['action'] == 'disconnect_onboarding' ) {
                $account_id = get_user_meta( $current_user->ID, 'account_id', true );
                $is_onboard = get_user_meta( $current_user->ID, 'is_onboard', true );
                if ( !empty( $account_id ) ) {
                    $stripe = new \Stripe\StripeClient($stripe_key);
                    try {
                        $wpdb->delete( $wpdb->prefix . SPP_STRP_ACCNT_TABLE, array(
                            'bsd_account_id' => $account_id,
                        ), array('%s') );
                        update_user_meta( $current_user->ID, 'account_id', '' );
                        update_user_meta( $current_user->ID, 'is_onboard', '0' );
                        wp_safe_redirect( admin_url( '/admin.php?page=split-pay-stripe-connect&msg=ob_disconnect_success' ) );
                        exit;
                    } catch ( \Exception $e ) {
                        $error = $e->getError();
                        $error_type = $error->type;
                        $error_message = $error->message;
                        $error_request_log_url = $error->request_log_url;
                        \BSD_Split_Pay_Stripe_Connect_Woo\spp_safe_wcs_log( $this->log_prefix . ': error_type ' . wp_json_encode( $error_type ) );
                        \BSD_Split_Pay_Stripe_Connect_Woo\spp_safe_wcs_log( $this->log_prefix . ': error_message ' . wp_json_encode( $error_message ) );
                        \BSD_Split_Pay_Stripe_Connect_Woo\spp_safe_wcs_log( $this->log_prefix . ': error_request_log_url ' . wp_json_encode( $error_request_log_url ) );
                        set_transient( 'ob_failed', $error_message, 120 );
                        wp_safe_redirect( admin_url( '/admin.php?page=split-pay-stripe-connect&msg=ob_failed' ) );
                        exit;
                    }
                }
            }
        }

        public function get_random_email( $username_length = 6 ) {
            $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
            $randomName = '';
            for ($j = 0; $j < $username_length; $j++) {
                $randomName .= $characters[wp_rand( 0, strlen( $characters ) - 1 )];
            }
            $fullAddress = $randomName . '@' . 'mailinator.com';
            return $fullAddress;
        }

        public function get_stripe_mode() {
            $stripe_mode = 'live';
            $woo_stripe_settings = get_option( 'woocommerce_stripe_settings' );
            if ( isset( $woo_stripe_settings['testmode'] ) ) {
                $stripe_mode = ( $woo_stripe_settings['testmode'] == 'yes' ? 'test' : 'live' );
            }
            return $stripe_mode;
        }

        /**
         * The Stripe mode an account is being connected in *right now*.
         *
         * Used to stamp bsd_strp_accnt.bsd_account_mode at every sync /
         * onboard / manual-add insert (3.7.5). The key prefix is the source
         * of truth — an `sk_test_`/`rk_test_` key produces test-mode Connect
         * accounts no matter what the testmode flag says, and vice-versa — so
         * we read the actual secret key SCA uses (stp_retrieve_keys(), which
         * selects by woocommerce_stripe_settings['testmode']) and classify by
         * prefix. If the key is missing/unrecognised we fall back to the
         * gateway's testmode flag via get_stripe_mode(), so the result is
         * always 'test' or 'live'.
         *
         * @since 3.7.5
         * @return string 'test'|'live'
         */
        public function current_connect_mode() {
            $keys = $this->stp_retrieve_keys();
            $secret_key = ( is_array( $keys ) && !empty( $keys['secret_key'] ) ? $keys['secret_key'] : '' );
            if ( is_string( $secret_key ) && $secret_key !== '' ) {
                if ( strpos( $secret_key, 'sk_test_' ) === 0 || strpos( $secret_key, 'rk_test_' ) === 0 ) {
                    return 'test';
                }
                if ( strpos( $secret_key, 'sk_live_' ) === 0 || strpos( $secret_key, 'rk_live_' ) === 0 ) {
                    return 'live';
                }
            }
            // Unknown / unset key — fall back to the gateway testmode flag.
            return $this->get_stripe_mode();
        }

        /**
         * One-time backfill of bsd_account_mode for accounts cached before the
         * column existed (migration-377). For each undetected ('') row we ask
         * Stripe which mode the account actually lives in: retrieve it with the
         * platform's TEST key (success ⇒ test) then the LIVE key (success ⇒
         * live); if neither key can see it, mark 'unknown'. The key prefix that
         * successfully resolves the account is the source of truth — Stripe
         * itself rejects a cross-mode retrieve with resource_missing.
         *
         * Safety / cost controls:
         *   - Sentinel option `bsd_split_pay_377_modes_detected` short-circuits
         *     once every row is resolved (set only when no '' rows remain).
         *   - A 90s transient lock bounds work to one pass per 90s, so large
         *     sites backfill over a few admin loads instead of hammering Stripe
         *     on every page.
         *   - Capped at 25 retrieves per pass.
         *   - If NO platform key is configured we bail WITHOUT marking anything
         *     and WITHOUT setting the sentinel, so reconciliation resumes once a
         *     key is entered. Read-only against Stripe; wrapped so it can never
         *     fatal an admin page.
         *
         * @since 3.7.5
         * @return void
         */
        public function reconcile_account_modes() {
            try {
                if ( get_option( 'bsd_split_pay_377_modes_detected', false ) ) {
                    return;
                }
                if ( function_exists( 'current_user_can' ) && !current_user_can( 'manage_woocommerce' ) ) {
                    return;
                }
                // Throttle: at most one pass per 90s while incomplete.
                if ( get_transient( 'spp_377_reconcile_lock' ) ) {
                    return;
                }
                set_transient( 'spp_377_reconcile_lock', 1, 90 );
                global $wpdb;
                $table = $wpdb->prefix . SPP_STRP_ACCNT_TABLE;
                // Column must exist (migration-377 runs first); bail quietly if not.
                $has_col = $wpdb->get_var( $wpdb->prepare( 'SELECT COUNT(*) FROM information_schema.COLUMNS
					 WHERE table_schema = DATABASE() AND table_name = %s AND column_name = %s', $table, 'bsd_account_mode' ) );
                if ( (int) $has_col === 0 ) {
                    return;
                }
                // Gather candidate platform keys for each mode (deduped, non-empty).
                $wc = get_option( 'woocommerce_stripe_settings' );
                $wc = ( is_array( $wc ) ? $wc : array() );
                $test_keys = array_values( array_unique( array_filter( array((string) get_option( 'spp_wc_stripe_test_secret_key', '' ), (string) get_option( 'stripe_test_api_secret_key', '' ), ( isset( $wc['test_secret_key'] ) ? (string) $wc['test_secret_key'] : '' )) ) ) );
                $live_keys = array_values( array_unique( array_filter( array((string) get_option( 'spp_wc_stripe_live_secret_key', '' ), (string) get_option( 'stripe_api_secret_key', '' ), ( isset( $wc['secret_key'] ) ? (string) $wc['secret_key'] : '' )) ) ) );
                // No key at all → can't detect anything; leave rows '' and retry
                // on a future load once a key is configured.
                if ( empty( $test_keys ) && empty( $live_keys ) ) {
                    return;
                }
                $rows = $wpdb->get_results( "SELECT bsd_sat_id, bsd_account_id FROM {$table} WHERE bsd_account_mode = '' LIMIT 25", ARRAY_A );
                // Nothing left to detect → mark complete so we stop checking.
                if ( empty( $rows ) ) {
                    update_option( 'bsd_split_pay_377_modes_detected', '1' );
                    return;
                }
                if ( file_exists( SPP_PLUGIN_DIR . '/includes/vendor/autoload.php' ) ) {
                    require_once SPP_PLUGIN_DIR . '/includes/vendor/autoload.php';
                }
                if ( !class_exists( '\\Stripe\\StripeClient' ) ) {
                    return;
                    // SDK unavailable; try again next pass.
                }
                foreach ( $rows as $row ) {
                    $account_id = ( isset( $row['bsd_account_id'] ) ? trim( (string) $row['bsd_account_id'] ) : '' );
                    $sat_id = (int) $row['bsd_sat_id'];
                    // Blank/garbage id can't be resolved — flag unknown so it
                    // stops blocking the sentinel.
                    if ( $account_id === '' || strpos( $account_id, 'acct_' ) !== 0 ) {
                        $wpdb->update( $table, array(
                            'bsd_account_mode' => 'unknown',
                        ), array(
                            'bsd_sat_id' => $sat_id,
                        ) );
                        continue;
                    }
                    $detected = $this->detect_account_mode( $account_id, $test_keys, $live_keys );
                    $wpdb->update( $table, array(
                        'bsd_account_mode' => $detected,
                    ), array(
                        'bsd_sat_id' => $sat_id,
                    ) );
                }
                // Finalize as soon as no undetected rows remain (every account is
                // now test/live/unknown). For > 25 blanks a single pass can't
                // clear them all, so the sentinel stays unset and the next
                // eligible pass (after the lock expires) continues — the
                // empty-rows branch above is the other place it completes.
                $remaining = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$table} WHERE bsd_account_mode = ''" );
                if ( $remaining === 0 ) {
                    update_option( 'bsd_split_pay_377_modes_detected', '1' );
                }
            } catch ( \Throwable $e ) {
                if ( class_exists( '\\WC_Stripe_Logger' ) && method_exists( '\\WC_Stripe_Logger', 'debug' ) ) {
                    \WC_Stripe_Logger::debug( '[SPP reconcile_account_modes] ' . $e->getMessage() );
                }
            }
        }

        /**
         * Resolve one account's Stripe mode by attempting a retrieve with each
         * candidate key. First key whose retrieve succeeds wins (test before
         * live). 'unknown' if neither mode can see the account.
         *
         * @since 3.7.5
         * @param string   $account_id Stripe Connect account id (acct_…).
         * @param string[] $test_keys  Candidate test-mode secret keys.
         * @param string[] $live_keys  Candidate live-mode secret keys.
         * @return string 'test'|'live'|'unknown'
         */
        private function detect_account_mode( $account_id, array $test_keys, array $live_keys ) {
            foreach ( array(
                'test' => $test_keys,
                'live' => $live_keys,
            ) as $mode => $keys ) {
                foreach ( $keys as $key ) {
                    try {
                        $client = new \Stripe\StripeClient($key);
                        $acct = $client->accounts->retrieve( $account_id, array() );
                        if ( !empty( $acct ) && isset( $acct->id ) && $acct->id === $account_id ) {
                            return $mode;
                        }
                    } catch ( \Throwable $e ) {
                        // resource_missing / permission / auth error ⇒ this key
                        // can't see the account; try the next candidate.
                        continue;
                    }
                }
            }
            return 'unknown';
        }

        public function export_to_csv() {
            if ( isset( $_GET['action'] ) && $_GET['action'] == 'export_to_csv' ) {
                if ( check_admin_referer( 'action=export_to_csv', 'security' ) ) {
                    global $wpdb;
                    $table_name = $wpdb->prefix . SPP_TRANSFER_LOG_TABLE;
                    $stripe_mode = $this->get_stripe_mode();
                    $sql = "SELECT COUNT(id) FROM {$table_name} WHERE stripe_mode = %s";
                    // $table_name is the constant log table ($wpdb->prefix . SPP_TRANSFER_LOG_TABLE); $stripe_mode is bound via the %s placeholder in prepare().
                    $result = $wpdb->get_var( $wpdb->prepare( $sql, $stripe_mode ) );
                    // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- constant table identifier; user value bound via %s placeholder.
                    if ( !empty( $result ) ) {
                        $total_rows = $result;
                        $per_page = 50;
                        $total_pages = ceil( $total_rows / $per_page );
                        if ( !empty( $total_pages ) ) {
                            $this->download_send_headers( 'stripe_transfer_log_' . gmdate( 'Y-m-d' ) . '.csv' );
                            for ($i = 0; $i <= $total_pages; $i++) {
                                $paged = $i;
                                $offset = $paged * $per_page;
                                $orderby = 'id';
                                $order = 'desc';
                                $sql = "SELECT\n\t\t\t\t\t\t\t\t\twc_order_id,\n\t\t\t\t\t\t\t\t\tcharge_date,\n\t\t\t\t\t\t\t\t\tintegration_source,\n\t\t\t\t\t\t\t\t\ttransfer_type,\n\t\t\t\t\t\t\t\t\ttransfer_type as level,\n\t\t\t\t\t\t\t\t\ttransfer_entered_value,\n\t\t\t\t\t\t\t\t\ttransfer_type as entered_variable,\n\t\t\t\t\t\t\t\t\titem_total,\n\t\t\t\t\t\t\t\t\t(transfer_amount - transfer_tax_value) as item_transfer_amount,\n\t\t\t\t\t\t\t\t\titem_tax_total,\n\t\t\t\t\t\t\t\t\ttax_transfer_type,\n\t\t\t\t\t\t\t\t\ttransfer_tax_value,\n\t\t\t\t\t\t\t\t\ttransfer_amount,\n\t\t\t\t\t\t\t\t\ttransfer_destination,\n\t\t\t\t\t\t\t\t\ttransfer_id,\n\t\t\t\t\t\t\t\t\tcharge_id\n\t\t\t\t\t\t\t\tFROM {$table_name}\n\t\t\t\t\t\t\t\tWHERE stripe_mode = %s\n\t\t\t\t\t\t\t\tORDER BY " . sanitize_sql_orderby( $orderby . ' ' . $order ) . '
								LIMIT %d OFFSET %d';
                                // $table_name is the constant log table; ORDER BY runs through sanitize_sql_orderby(); $stripe_mode/$per_page/$offset are bound via prepare() placeholders.
                                $query_results = $wpdb->get_results( $wpdb->prepare(
                                    $sql,
                                    $stripe_mode,
                                    $per_page,
                                    $offset
                                ), ARRAY_A );
                                // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- constant table identifier + sanitize_sql_orderby(); user values bound via placeholders.
                                if ( $paged == 0 ) {
                                    $column_header = array(
                                        'Order ID',
                                        'Date',
                                        'Source',
                                        'Item',
                                        'Level',
                                        'Value',
                                        'Variable',
                                        'Item Total',
                                        'Item Transfer Amount',
                                        'Item Tax',
                                        'Tax Transfer Type',
                                        'Tax Transfer Amount',
                                        'Total Transfer Amount',
                                        'Connected Account',
                                        'Stripe Transfer ID',
                                        'Stripe Charge ID'
                                    );
                                    // Raw CSV file output (text/csv download via download_send_headers()
                                    // above) — not HTML. HTML-escaping here would corrupt the CSV
                                    // data, and there is no HTML/XSS surface in a downloaded CSV.
                                    echo $this->array2csv( $query_results, $column_header, true );
                                    // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
                                } else {
                                    echo $this->array2csv( $query_results, array(), false );
                                    // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
                                }
                            }
                        }
                    }
                    die;
                    // wp_redirect(admin_url('admin.php?page=bsd-split-pay-stripe-connect-woo-settings&tab=transfers')); die;
                }
            }
        }

        public function array2csv( array &$array, $column_header, $is_header = false ) {
            if ( count( $array ) == 0 ) {
                return null;
            }
            ob_start();
            $df = fopen( 'php://output', 'w' );
            if ( $is_header ) {
                fputcsv( $df, $column_header );
            }
            foreach ( $array as $row ) {
                $tt = (int) $row['transfer_type'];
                $transfer_type = '';
                switch ( $tt ) {
                    case 0:
                        $transfer_type = 'Subtotal';
                        break;
                    case 1:
                    case 2:
                    case 5:
                    case 8:
                    case 9:
                    case 10:
                    case 11:
                        $transfer_type = 'Subtotal';
                        break;
                    case 3:
                    case 4:
                    case 6:
                    case 7:
                        $transfer_type = 'Shipping';
                        break;
                }
                $entered_variable = '';
                switch ( $tt ) {
                    case 0:
                        $entered_variable = ( !empty( $row['transfer_entered_value'] ) ? 'Percentage' : '' );
                        break;
                    case 1:
                    case 3:
                    case 5:
                    case 6:
                    case 8:
                    case 10:
                        $entered_variable = 'Percentage';
                        break;
                    case 2:
                    case 4:
                    case 7:
                    case 9:
                    case 11:
                        $entered_variable = 'Fixed';
                        break;
                }
                $level = '';
                switch ( $tt ) {
                    case 0:
                        $level = ( !empty( $row['transfer_entered_value'] ) ? 'Product' : '' );
                        break;
                    case 1:
                    case 2:
                    case 3:
                    case 4:
                        $level = 'Global';
                        break;
                    case 5:
                    case 6:
                    case 7:
                    case 8:
                    case 9:
                        $level = 'Product';
                        break;
                    case 10:
                    case 11:
                        $level = 'Variable';
                        break;
                }
                $source_labels = [
                    'woocommerce' => 'WooCommerce',
                    'fluentcart'  => 'FluentCart',
                ];
                $source_val = $source_labels[$row['integration_source'] ?? ''] ?? $row['integration_source'] ?? '';
                $tax_type_labels = [
                    'all'          => 'All (100%)',
                    'proportional' => 'Proportional',
                    ''             => '',
                ];
                $tax_type_val = $tax_type_labels[$row['tax_transfer_type'] ?? ''] ?? $row['tax_transfer_type'] ?? '';
                $refectored_row = array(
                    'wc_order_id'            => $row['wc_order_id'],
                    'charge_date'            => $row['charge_date'],
                    'integration_source'     => $source_val,
                    'transfer_type'          => $transfer_type,
                    'level'                  => $level,
                    'transfer_entered_value' => $row['transfer_entered_value'],
                    'entered_variable'       => $entered_variable,
                    'item_total'             => $row['item_total'] ?? '',
                    'item_transfer_amount'   => $row['item_transfer_amount'] ?? '',
                    'item_tax_total'         => $row['item_tax_total'] ?? '',
                    'tax_transfer_type'      => $tax_type_val,
                    'transfer_tax_value'     => $row['transfer_tax_value'] ?? '',
                    'transfer_amount'        => $row['transfer_amount'],
                    'transfer_destination'   => $row['transfer_destination'],
                    'transfer_id'            => $row['transfer_id'],
                    'charge_id'              => $row['charge_id'],
                );
                fputcsv( $df, $refectored_row );
            }
            // $df is the PHP output stream (fopen 'php://output'), not a filesystem path; WP_Filesystem cannot operate on php://output. fclose is the correct paired close.
            fclose( $df );
            // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- php://output stream, not a real file.
            return ob_get_clean();
        }

        public function download_send_headers( $filename ) {
            // disable caching
            $now = gmdate( 'D, d M Y H:i:s' );
            header( 'Expires: Tue, 03 Jul 2001 06:00:00 GMT' );
            header( 'Cache-Control: max-age=0, no-cache, must-revalidate, proxy-revalidate' );
            header( "Last-Modified: {$now} GMT" );
            // force download
            header( 'Content-Type: application/force-download' );
            header( 'Content-Type: application/octet-stream' );
            header( 'Content-Type: application/download' );
            // disposition / encoding on response body
            header( "Content-Disposition: attachment;filename={$filename}" );
            header( 'Content-Transfer-Encoding: binary' );
        }

        public function update_connected_id_table() {
            $bsd_connected_id_table_created = get_option( 'bsd_connected_id_table_created', false );
            if ( !empty( $bsd_connected_id_table_created ) && $bsd_connected_id_table_created == '1' ) {
                global $wpdb;
                $table_name = $wpdb->prefix . SPP_CONNECTED_ID_TABLE;
                // All queries in this schema-migration block interpolate only the constant table name ($wpdb->prefix . SPP_CONNECTED_ID_TABLE) into DDL/SHOW statements; there is no user input and nothing bindable for prepare().
                try {
                    if ( $wpdb->get_var( "SHOW TABLES LIKE '" . $table_name . "'" ) == $table_name ) {
                        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- constant table identifier, no user input.
                        $wpdb->query( "ALTER TABLE {$table_name} ADD COLUMN `bsd_global_shipping_type` varchar(50) DEFAULT '' NOT NULL" );
                        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- DDL on constant table identifier.
                        $wpdb->query( "ALTER TABLE {$table_name} ADD COLUMN `bsd_global_shipping_percentage_amount` double DEFAULT 0 NOT NULL" );
                        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- DDL on constant table identifier.
                        $first_row = $wpdb->get_row( "SELECT * FROM {$table_name} order by bsd_connected_id asc limit 1", ARRAY_A );
                        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- constant table identifier, no user input.
                        if ( !empty( $first_row ) ) {
                            $wpdb->update(
                                $table_name,
                                array(
                                    'bsd_global_shipping_type'              => 'percentage',
                                    'bsd_global_shipping_percentage_amount' => 100,
                                ),
                                array(
                                    'bsd_connected_id' => $first_row['bsd_connected_id'],
                                ),
                                array('%s', '%d'),
                                array('%d')
                            );
                        }
                        update_option( 'bsd_connected_id_table_created', '1.1' );
                    }
                } catch ( \Exception $e ) {
                    if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
                        error_log( '[BSD Split Pay for Stripe Connect Activation] Error updating database table: ' . $table_name );
                    }
                    // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
                }
            }
        }

        public function update_log_table() {
            $bsd_log_table_created = get_option( 'bsd_log_table_created', false );
            if ( empty( $bsd_log_table_created ) ) {
                global $wpdb;
                $table_name = $wpdb->prefix . SPP_TRANSFER_LOG_TABLE;
                try {
                    // Schema migration: $table_name is the constant log table ($wpdb->prefix . SPP_TRANSFER_LOG_TABLE); SHOW/ALTER take a constant identifier with no bindable user input.
                    if ( $wpdb->get_var( "SHOW TABLES LIKE '" . $table_name . "'" ) == $table_name ) {
                        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- constant table identifier, no user input.
                        $wpdb->query( 'ALTER TABLE ' . $table_name . ' ADD `transfer_type` tinyint(2) NOT NULL AFTER `stripe_mode`' );
                        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- DDL on constant table identifier.
                        update_option( 'bsd_log_table_created', '1.1' );
                    }
                } catch ( \Exception $e ) {
                    if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
                        error_log( '[BSD Split Pay for Stripe Connect Activation] Error updating database table: ' . $table_name );
                    }
                    // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
                }
            }
            if ( !empty( $bsd_log_table_created ) && $bsd_log_table_created == '1.1' ) {
                global $wpdb;
                $table_name = $wpdb->prefix . SPP_TRANSFER_LOG_TABLE;
                try {
                    // Schema migration: $table_name is the constant log table ($wpdb->prefix . SPP_TRANSFER_LOG_TABLE); SHOW/ALTER take a constant identifier with no bindable user input.
                    if ( $wpdb->get_var( "SHOW TABLES LIKE '" . $table_name . "'" ) == $table_name ) {
                        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- constant table identifier, no user input.
                        $wpdb->query( 'ALTER TABLE ' . $table_name . ' ADD `transfer_entered_value` decimal(10,2) NULL AFTER `transfer_amount`' );
                        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- DDL on constant table identifier.
                        update_option( 'bsd_log_table_created', '1.2' );
                    }
                } catch ( \Exception $e ) {
                    if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
                        error_log( '[BSD Split Pay for Stripe Connect Activation] Error updating database table: ' . $table_name );
                    }
                    // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
                }
            }
            if ( !empty( $bsd_log_table_created ) && $bsd_log_table_created == '1.2' ) {
                global $wpdb;
                $table_name = $wpdb->prefix . SPP_TRANSFER_LOG_TABLE;
                try {
                    // Schema migration: $table_name is the constant log table ($wpdb->prefix . SPP_TRANSFER_LOG_TABLE); SHOW/ALTER take a constant identifier with no bindable user input.
                    if ( $wpdb->get_var( "SHOW TABLES LIKE '" . $table_name . "'" ) == $table_name ) {
                        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- constant table identifier, no user input.
                        $wpdb->query( 'ALTER TABLE ' . $table_name . ' ADD `transfer_tax_value` decimal(10,2) NULL AFTER `transfer_amount`' );
                        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- DDL on constant table identifier.
                        update_option( 'bsd_log_table_created', '1.3' );
                    }
                } catch ( \Exception $e ) {
                    if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
                        error_log( '[BSD Split Pay for Stripe Connect Activation] Error updating database table: ' . $table_name );
                    }
                    // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
                }
            }
            if ( !empty( $bsd_log_table_created ) && $bsd_log_table_created == '1.3' ) {
                global $wpdb;
                $table_name = $wpdb->prefix . SPP_TRANSFER_LOG_TABLE;
                try {
                    // Schema migration: $table_name is the constant log table ($wpdb->prefix . SPP_TRANSFER_LOG_TABLE); SHOW/ALTER take a constant identifier with no bindable user input.
                    if ( $wpdb->get_var( "SHOW TABLES LIKE '" . $table_name . "'" ) == $table_name ) {
                        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- constant table identifier, no user input.
                        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- DDL on constant table identifier ($wpdb->prefix . SPP_TRANSFER_LOG_TABLE); no bindable user input.
                        $wpdb->query( 'ALTER TABLE ' . $table_name . '
						ADD column `item_total` decimal(10,2) NULL AFTER `transfer_entered_value`,
						ADD column `item_transfer_amount` decimal(10,2) NULL AFTER `transfer_entered_value`,
						ADD column `item_tax_total` decimal(10,2) NULL AFTER `transfer_entered_value`,
						ADD column `tax_transfer_type` char(5) NOT NULL AFTER `transfer_entered_value`' );
                        update_option( 'bsd_log_table_created', '1.4' );
                    }
                } catch ( \Exception $e ) {
                    if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
                        error_log( '[BSD Split Pay for Stripe Connect Activation] Error updating database table: ' . $table_name );
                    }
                    // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
                }
            }
        }

        public function update_log_table_integration_source() {
            $bsd_log_table_integration_source = get_option( 'bsd_log_table_integration_source', false );
            if ( empty( $bsd_log_table_integration_source ) ) {
                global $wpdb;
                $table_name = $wpdb->prefix . SPP_TRANSFER_LOG_TABLE;
                try {
                    // Schema migration: $table_name is the constant log table ($wpdb->prefix . SPP_TRANSFER_LOG_TABLE); SHOW/ALTER take a constant identifier with no bindable user input.
                    if ( $wpdb->get_var( "SHOW TABLES LIKE '" . $table_name . "'" ) == $table_name ) {
                        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- constant table identifier, no user input.
                        $wpdb->query( "ALTER TABLE {$table_name} ADD COLUMN `integration_source` VARCHAR(30) DEFAULT 'woocommerce' NOT NULL, ADD COLUMN `source_order_id` VARCHAR(50) DEFAULT NULL" );
                        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- DDL on constant table identifier.
                        update_option( 'bsd_log_table_integration_source', '1.0' );
                    }
                } catch ( \Exception $e ) {
                    if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
                        error_log( '[BSD Split Pay for Stripe Connect Activation] Error updating database table: ' . $table_name );
                    }
                    // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
                }
            }
        }

        public function update_account_id_table() {
            $spp_account_id_table = get_option( 'spp_account_id_table', false );
            if ( empty( $spp_account_id_table ) ) {
                global $wpdb;
                $table_name = $wpdb->prefix . SPP_STRP_ACCNT_TABLE;
                try {
                    // Schema migration: $table_name is the constant account table ($wpdb->prefix . SPP_STRP_ACCNT_TABLE); SHOW/ALTER take a constant identifier with no bindable user input.
                    if ( $wpdb->get_var( "SHOW TABLES LIKE '" . $table_name . "'" ) == $table_name ) {
                        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- constant table identifier, no user input.
                        $wpdb->query( 'ALTER TABLE ' . $table_name . ' ADD `bsd_account_email` varchar(255) DEFAULT "" NOT NULL AFTER `bsd_account_name`' );
                        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- DDL on constant table identifier.
                        update_option( 'spp_account_id_table', '1.1' );
                    }
                } catch ( \Exception $e ) {
                    if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
                        error_log( '[BSD Split Pay for Stripe Connect Activation] Error updating database table: ' . $table_name );
                    }
                    // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
                }
            }
        }

        public function create_connect_id_table() {
            $bsd_connected_id_table_created = get_option( 'bsd_connected_id_table_created', false );
            if ( empty( $bsd_connected_id_table_created ) ) {
                require_once ABSPATH . 'wp-admin/includes/upgrade.php';
                global $wpdb;
                $table_prefix = $wpdb->prefix;
                $table_name = $table_prefix . SPP_CONNECTED_ID_TABLE;
                $charset_collate = $wpdb->get_charset_collate();
                try {
                    // $table_name is the constant connected-id table ($wpdb->prefix . SPP_CONNECTED_ID_TABLE); SHOW takes a constant identifier with no bindable user input.
                    if ( $wpdb->get_var( "SHOW TABLES LIKE '" . $table_name . "'" ) != $table_name ) {
                        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- constant table identifier, no user input.
                        $sql = 'CREATE TABLE IF NOT EXISTS ' . $table_name . " (\n                                bsd_connected_id bigint(20) NOT NULL AUTO_INCREMENT,\n                                bsd_connected_account_id varchar(55) DEFAULT '' NOT NULL,\n                                bsd_spscwt_type varchar(50) DEFAULT '' NOT NULL,\n                                bsd_spscwt_percentage_amount double DEFAULT 0 NOT NULL,\n                                bsd_global_shipping_type varchar(50) DEFAULT '' NOT NULL,\n                                bsd_global_shipping_percentage_amount double DEFAULT 0 NOT NULL,\n                                PRIMARY KEY  (bsd_connected_id)\n                            ) {$charset_collate};";
                        $is_table_created = dbDelta( $sql );
                        if ( !empty( $is_table_created ) ) {
                            update_option( 'bsd_connected_id_table_created', '1' );
                        }
                        $bsd_split_pay_stripe_connect_woo_stripe_connected_account = get_option( 'bsd_split_pay_stripe_connect_woo_stripe_connected_account' );
                        $bsd_spscwt_type = get_option( 'bsd_spscwt_type' );
                        if ( !empty( $bsd_split_pay_stripe_connect_woo_stripe_connected_account ) && !empty( $bsd_spscwt_type ) ) {
                            $percentage_or_amount = 0;
                            if ( $bsd_spscwt_type == 'percentage' ) {
                                $bsd_split_pay_stripe_connect_woo_transfer_percentage = get_option( 'bsd_split_pay_stripe_connect_woo_transfer_percentage' );
                                $percentage_or_amount = $bsd_split_pay_stripe_connect_woo_transfer_percentage;
                            } else {
                                $bsd_spscwt_amount = get_option( 'bsd_spscwt_amount' );
                                $percentage_or_amount = $bsd_spscwt_amount;
                            }
                            $wpdb->insert( $table_name, array(
                                'bsd_connected_account_id'     => $bsd_split_pay_stripe_connect_woo_stripe_connected_account,
                                'bsd_spscwt_type'              => $bsd_spscwt_type,
                                'bsd_spscwt_percentage_amount' => $percentage_or_amount,
                            ), array('%s', '%s', '%d') );
                        }
                    }
                } catch ( \Exception $e ) {
                    if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
                        error_log( '[BSD Split Pay for Stripe Connect Activation] Error creating database table: ' . $table_name );
                    }
                    // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
                }
            }
        }

        public function store_selected_accounts() {
            // 3.7.5 static-analysis: $_POST always exists in PHP.
            if ( !empty( $_POST ) ) {
                if ( isset( $_POST['_wpnonce'] ) && wp_verify_nonce( $_POST['_wpnonce'], 'bsd_scsp_options_main-options' ) ) {
                    if ( isset( $_POST['option_page'] ) && $_POST['option_page'] == 'bsd_scsp_options_main' ) {
                        global $wpdb;
                        $table_prefix = $wpdb->prefix;
                        $table_name = $table_prefix . SPP_CONNECTED_ID_TABLE;
                        $insert_array = array();
                        if ( isset( $_POST['bsd_split_pay_stripe_connect_woo_stripe_connected_account'] ) && !empty( $_POST['bsd_split_pay_stripe_connect_woo_stripe_connected_account'] ) && isset( $_POST['bsd_spscwt_type'] ) && !empty( $_POST['bsd_spscwt_type'] ) ) {
                            foreach ( wp_unslash( $_POST['bsd_split_pay_stripe_connect_woo_stripe_connected_account'] ) as $bspscwsca_key => $bspscwsca_value ) {
                                $percentage_or_amount = '0';
                                $bsd_global_shipping_percentage_amount = '0';
                                $insert_array[$bspscwsca_key]['account_id'] = sanitize_text_field( $bspscwsca_value );
                                if ( isset( $_POST['bsd_connected_acc_primary_id'][$bspscwsca_key] ) && !empty( $_POST['bsd_connected_acc_primary_id'][$bspscwsca_key] ) ) {
                                    $insert_array[$bspscwsca_key]['id'] = sanitize_text_field( wp_unslash( $_POST['bsd_connected_acc_primary_id'][$bspscwsca_key] ) );
                                }
                                if ( !empty( $insert_array[$bspscwsca_key]['account_id'] ) ) {
                                    if ( isset( $_POST['bsd_spscwt_type'][$bspscwsca_key] ) && !empty( $_POST['bsd_spscwt_type'][$bspscwsca_key] ) ) {
                                        $insert_array[$bspscwsca_key]['bsd_spscwt_type'] = sanitize_text_field( wp_unslash( $_POST['bsd_spscwt_type'][$bspscwsca_key] ) );
                                    }
                                    if ( $insert_array[$bspscwsca_key]['bsd_spscwt_type'] == 'percentage' ) {
                                        if ( isset( $_POST['bsd_split_pay_stripe_connect_woo_transfer_percentage'][$bspscwsca_key] ) ) {
                                            $percentage_or_amount = sanitize_text_field( wp_unslash( $_POST['bsd_split_pay_stripe_connect_woo_transfer_percentage'][$bspscwsca_key] ) );
                                        }
                                    } elseif ( $insert_array[$bspscwsca_key]['bsd_spscwt_type'] == 'amount' ) {
                                        if ( isset( $_POST['bsd_spscwt_amount'][$bspscwsca_key] ) ) {
                                            $percentage_or_amount = sanitize_text_field( wp_unslash( $_POST['bsd_spscwt_amount'][$bspscwsca_key] ) );
                                        }
                                    }
                                    if ( $percentage_or_amount === '' ) {
                                        $percentage_or_amount = '0';
                                    }
                                    if ( isset( $_POST['bsd_global_shipping_type'][$bspscwsca_key] ) && !empty( $_POST['bsd_global_shipping_type'][$bspscwsca_key] ) ) {
                                        $insert_array[$bspscwsca_key]['bsd_global_shipping_type'] = sanitize_text_field( wp_unslash( $_POST['bsd_global_shipping_type'][$bspscwsca_key] ) );
                                    }
                                    if ( isset( $insert_array[$bspscwsca_key]['bsd_global_shipping_type'] ) && $insert_array[$bspscwsca_key]['bsd_global_shipping_type'] == 'percentage' ) {
                                        if ( isset( $_POST['bsd_global_shipping_percentage'][$bspscwsca_key] ) ) {
                                            $bsd_global_shipping_percentage_amount = sanitize_text_field( wp_unslash( $_POST['bsd_global_shipping_percentage'][$bspscwsca_key] ) );
                                        }
                                    } elseif ( isset( $insert_array[$bspscwsca_key]['bsd_global_shipping_type'] ) && $insert_array[$bspscwsca_key]['bsd_global_shipping_type'] == 'amount' ) {
                                        if ( isset( $_POST['bsd_global_shipping_amount'][$bspscwsca_key] ) ) {
                                            $bsd_global_shipping_percentage_amount = sanitize_text_field( wp_unslash( $_POST['bsd_global_shipping_amount'][$bspscwsca_key] ) );
                                        }
                                    }
                                    if ( $bsd_global_shipping_percentage_amount === '' ) {
                                        $bsd_global_shipping_percentage_amount = '0';
                                    }
                                    $insert_array[$bspscwsca_key]['percentage_or_amount'] = $percentage_or_amount;
                                    $insert_array[$bspscwsca_key]['bsd_global_shipping_percentage_amount'] = $bsd_global_shipping_percentage_amount;
                                }
                            }
                            if ( !empty( $insert_array ) ) {
                                $insert_array = apply_filters( 'spp_global_connect_id_settings', $insert_array );
                                foreach ( $insert_array as $irk ) {
                                    if ( isset( $irk['account_id'] ) && empty( $irk['account_id'] ) ) {
                                        if ( isset( $irk['id'] ) && !empty( $irk['id'] ) ) {
                                            $wpdb->delete( $table_name, array(
                                                'bsd_connected_id' => $irk['id'],
                                            ), array('%d') );
                                        }
                                    }
                                    if ( isset( $irk['account_id'] ) && !empty( $irk['account_id'] ) && isset( $irk['bsd_spscwt_type'] ) && !empty( $irk['bsd_spscwt_type'] ) && isset( $irk['percentage_or_amount'] ) ) {
                                        if ( isset( $irk['id'] ) && !empty( $irk['id'] ) ) {
                                            $wpdb->update(
                                                $table_name,
                                                array(
                                                    'bsd_connected_account_id'              => $irk['account_id'],
                                                    'bsd_spscwt_type'                       => $irk['bsd_spscwt_type'],
                                                    'bsd_spscwt_percentage_amount'          => $irk['percentage_or_amount'],
                                                    'bsd_global_shipping_type'              => ( isset( $irk['bsd_global_shipping_type'] ) ? $irk['bsd_global_shipping_type'] : '' ),
                                                    'bsd_global_shipping_percentage_amount' => ( isset( $irk['bsd_global_shipping_percentage_amount'] ) ? $irk['bsd_global_shipping_percentage_amount'] : '' ),
                                                ),
                                                array(
                                                    'bsd_connected_id' => $irk['id'],
                                                ),
                                                array('%s', '%s', '%f'),
                                                array('%d')
                                            );
                                        } else {
                                            $wpdb->insert( $table_name, array(
                                                'bsd_connected_account_id'              => $irk['account_id'],
                                                'bsd_spscwt_type'                       => $irk['bsd_spscwt_type'],
                                                'bsd_spscwt_percentage_amount'          => $irk['percentage_or_amount'],
                                                'bsd_global_shipping_type'              => ( isset( $irk['bsd_global_shipping_type'] ) ? $irk['bsd_global_shipping_type'] : '' ),
                                                'bsd_global_shipping_percentage_amount' => ( isset( $irk['bsd_global_shipping_percentage_amount'] ) ? $irk['bsd_global_shipping_percentage_amount'] : '' ),
                                            ), array('%s', '%s', '%f') );
                                        }
                                    }
                                }
                            }
                        }
                        if ( isset( $_POST['bsd_connected_acc_primary_remove_ids'] ) && !empty( $_POST['bsd_connected_acc_primary_remove_ids'] ) ) {
                            $remove_connected_ids = explode( ', ', sanitize_text_field( wp_unslash( $_POST['bsd_connected_acc_primary_remove_ids'] ) ) );
                            // 3.7.5 static-analysis: explode() of a non-empty string is always non-empty array.
                            foreach ( $remove_connected_ids as $rcik ) {
                                $wpdb->delete( $table_name, array(
                                    'bsd_connected_id' => $rcik,
                                ), array('%d') );
                            }
                        }
                        if ( isset( $_POST['sending_meta'] ) && $_POST['sending_meta'] == '1' ) {
                            update_option( 'sending_meta', '1' );
                        } else {
                            update_option( 'sending_meta', '0' );
                        }
                        if ( isset( $_POST['transfer_taxes'] ) && $_POST['transfer_taxes'] == '1' ) {
                            update_option( 'transfer_taxes', '1' );
                        } else {
                            update_option( 'transfer_taxes', '0' );
                        }
                        if ( isset( $_POST['tax_transfer_type'] ) && !empty( $_POST['tax_transfer_type'] ) ) {
                            update_option( 'tax_transfer_type', sanitize_text_field( wp_unslash( $_POST['tax_transfer_type'] ) ) );
                        } else {
                            update_option( 'tax_transfer_type', '0' );
                        }
                        if ( isset( $_POST['transfer_email'] ) && $_POST['transfer_email'] == '1' ) {
                            update_option( 'transfer_email', '1' );
                        } else {
                            update_option( 'transfer_email', '0' );
                        }
                        if ( isset( $_POST['coupon_adjust_transfers'] ) && $_POST['coupon_adjust_transfers'] == '1' ) {
                            update_option( 'coupon_adjust_transfers', '1' );
                        } else {
                            update_option( 'coupon_adjust_transfers', '0' );
                        }
                        if ( isset( $_POST['spp_delayed_transfers_enabled'] ) && $_POST['spp_delayed_transfers_enabled'] == '1' ) {
                            update_option( 'spp_delayed_transfers_enabled', '1' );
                        } else {
                            update_option( 'spp_delayed_transfers_enabled', '0' );
                        }
                    }
                }
            }
        }

        public function spp_vendor_options_process() {
            // 3.7.5 static-analysis: $_POST always exists in PHP.
            if ( !empty( $_POST ) ) {
                if ( isset( $_POST['_wpnonce'] ) && wp_verify_nonce( $_POST['_wpnonce'], 'spp_vendor_options-options' ) ) {
                    if ( isset( $_POST['option_page'] ) && $_POST['option_page'] == 'spp_vendor_options' ) {
                        if ( isset( $_POST['vendor_onboading'] ) && $_POST['vendor_onboading'] == '1' ) {
                            update_option( 'vendor_onboading', '1' );
                        } else {
                            update_option( 'vendor_onboading', '0' );
                        }
                        if ( isset( $_POST['enable_title_description'] ) && $_POST['enable_title_description'] == '1' ) {
                            update_option( 'enable_title_description', '1' );
                        } else {
                            update_option( 'enable_title_description', '0' );
                        }
                        if ( isset( $_POST['onboarding_title'] ) && !empty( $_POST['onboarding_title'] ) ) {
                            update_option( 'onboarding_title', sanitize_text_field( wp_unslash( $_POST['onboarding_title'] ) ) );
                        } else {
                            update_option( 'onboarding_title', '' );
                        }
                        if ( isset( $_POST['onboarding_description'] ) && !empty( $_POST['onboarding_description'] ) ) {
                            update_option( 'onboarding_description', sanitize_textarea_field( wp_unslash( $_POST['onboarding_description'] ) ) );
                        } else {
                            update_option( 'onboarding_description', '' );
                        }
                        if ( isset( $_POST['vendor_onboading'] ) && $_POST['vendor_onboading'] == '1' ) {
                            $old_vendor_roles = get_option( 'spp_vendor_roles', array() );
                            if ( !is_array( $old_vendor_roles ) ) {
                                $old_vendor_roles = array();
                            }
                            $new_vendor_roles = array();
                            if ( isset( $_POST['spp_vendor_roles'] ) && is_array( $_POST['spp_vendor_roles'] ) ) {
                                $new_vendor_roles = array_map( 'sanitize_text_field', wp_unslash( $_POST['spp_vendor_roles'] ) );
                            }
                            $roles_to_remove = array_diff( $old_vendor_roles, $new_vendor_roles );
                            foreach ( $roles_to_remove as $role_slug ) {
                                $role = get_role( $role_slug );
                                if ( $role ) {
                                    $role->remove_cap( 'spp_custom_menu' );
                                }
                            }
                            $roles_to_add = array_diff( $new_vendor_roles, $old_vendor_roles );
                            foreach ( $roles_to_add as $role_slug ) {
                                $role = get_role( $role_slug );
                                if ( $role ) {
                                    $role->add_cap( 'spp_custom_menu' );
                                }
                            }
                            update_option( 'spp_vendor_roles', $new_vendor_roles );
                        }
                    }
                }
            }
        }

        public function bsd_connect_standard_account() {
            if ( isset( $_GET['bsd_action'] ) && $_GET['bsd_action'] == 'connect_standard_account' ) {
                if ( !is_user_logged_in() || !current_user_can( 'spp_custom_menu' ) ) {
                    return;
                }
                global $wpdb;
                $woocommerce_stripe_settings = get_option( 'woocommerce_stripe_settings' );
                $client_id = SPP_TEST_CONNECT_ID;
                $data_keys = $this->stp_retrieve_keys();
                $secret_key = $data_keys['secret_key'];
                if ( isset( $woocommerce_stripe_settings['testmode'] ) && $woocommerce_stripe_settings['testmode'] == 'yes' ) {
                    // $secret_key = $woocommerce_stripe_settings['test_secret_key'];
                } else {
                    // $secret_key = $woocommerce_stripe_settings['secret_key'];
                    $client_id = SPP_LIVE_CONNECT_ID;
                }
                if ( !empty( $woocommerce_stripe_settings ) && !empty( $secret_key ) ) {
                    $stripe = new \Stripe\StripeClient($secret_key);
                    $redirect_uri = site_url( '/' ) . '?bsd_action=get_code';
                    $url = 'https://connect.stripe.com/oauth/authorize?response_type=code';
                    $url = add_query_arg( 'client_id', $client_id, $url );
                    $url = add_query_arg( 'scope', 'read_write', $url );
                    $url = add_query_arg( 'redirect_uri', urlencode( $redirect_uri ), $url );
                    // Whitelist Stripe-hosted onboarding hosts so wp_safe_redirect()
                    // doesn't reject them.
                    $spp_stripe_hosts = function ( $hosts ) {
                        $hosts[] = 'connect.stripe.com';
                        $hosts[] = 'hooks.stripe.com';
                        $hosts[] = 'dashboard.stripe.com';
                        return $hosts;
                    };
                    add_filter( 'allowed_redirect_hosts', $spp_stripe_hosts );
                    wp_safe_redirect( esc_url_raw( $url ) );
                    remove_filter( 'allowed_redirect_hosts', $spp_stripe_hosts );
                    exit;
                }
            }
        }

        public function get_initial_accounts() {
            global $wpdb;
            // Constant table name ($wpdb->prefix . SPP_STRP_ACCNT_TABLE); no user input and nothing bindable in this fixed SELECT.
            $account_results = $wpdb->get_results( 'select * from ' . $wpdb->prefix . SPP_STRP_ACCNT_TABLE . ' limit 100', ARRAY_A );
            // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- constant table identifier, no user input.
            if ( empty( $account_results ) ) {
                $woocommerce_stripe_settings = get_option( 'woocommerce_stripe_settings' );
                if ( !empty( $woocommerce_stripe_settings ) ) {
                    $is_test_mode = isset( $woocommerce_stripe_settings['testmode'] ) && $woocommerce_stripe_settings['testmode'] == 'yes';
                    /*
                     * 3.7.5 (PT-3 + PT-6): Try each candidate platform key in
                     * priority order, validating that each one is a usable
                     * PLATFORM secret key before we commit to it.
                     *
                     * Why this matters: legacy installs accumulate stale keys
                     * in `stripe_api_secret_key` (e.g. Peter @ tpforge.co.uk
                     * had a key for `acct_1TTKt2L...` — a connected account
                     * on someone else's Stripe Connect platform). With the
                     * old "first non-empty key wins" resolver, that key was
                     * picked, `Account::list()` returned 0 (no error, because
                     * connected accounts can't have their own connected
                     * accounts), and `is_all_sca_fetched` got set to true —
                     * silently swallowing the real platform key further down
                     * the candidate list.
                     *
                     * New behavior:
                     *   1. For each candidate key, call `Account::retrieve()`
                     *      and inspect `controller.type`. Stripe's v2024+
                     *      API exposes `controller.type === "account"` for
                     *      standalone / self-controlled accounts (i.e. the
                     *      kind of account that can BE a platform).
                     *      `controller.type === "application"` means the
                     *      account is a connected account under another
                     *      platform — keys for those CANNOT list connected
                     *      accounts and we must move on.
                     *   2. If retrieve succeeds and looks like a platform,
                     *      call `Account::list()`. If it succeeds, use this
                     *      key.
                     *   3. If list throws Stripe's "cannot access the
                     *      connected accounts of your platform's connected
                     *      accounts" error, the key is OAuth-scoped — we
                     *      remember that and surface a friendly notice if
                     *      no later candidate works either.
                     *   4. If ALL candidates fail, fire the friendly notice
                     *      pointing the merchant at the SPP "Live/Test Mode
                     *      Secret Key" field on the Integrations tab.
                     */
                    $candidate_keys = array();
                    $candidate_keys[] = array(
                        'source'          => ( $is_test_mode ? 'spp_wc_stripe_test_secret_key' : 'spp_wc_stripe_live_secret_key' ),
                        'key'             => ( $is_test_mode ? (string) get_option( 'spp_wc_stripe_test_secret_key', '' ) : (string) get_option( 'spp_wc_stripe_live_secret_key', '' ) ),
                        'is_oauth_scoped' => false,
                    );
                    $candidate_keys[] = array(
                        'source'          => ( $is_test_mode ? 'stripe_test_api_secret_key' : 'stripe_api_secret_key' ),
                        'key'             => ( $is_test_mode ? (string) get_option( 'stripe_test_api_secret_key', '' ) : (string) get_option( 'stripe_api_secret_key', '' ) ),
                        'is_oauth_scoped' => false,
                    );
                    $candidate_keys[] = array(
                        'source'          => 'woocommerce_stripe_settings.' . (( $is_test_mode ? 'test_secret_key' : 'secret_key' )),
                        'key'             => ( $is_test_mode ? $woocommerce_stripe_settings['test_secret_key'] ?? '' : $woocommerce_stripe_settings['secret_key'] ?? '' ),
                        'is_oauth_scoped' => true,
                    );
                    $accounts = null;
                    $used_secret_key = '';
                    $saw_oauth_failure = false;
                    foreach ( $candidate_keys as $cand ) {
                        if ( empty( $cand['key'] ) ) {
                            continue;
                        }
                        try {
                            $stripe = new \Stripe\StripeClient($cand['key']);
                            $probe_account = $stripe->accounts->retrieve();
                            $controller = ( isset( $probe_account->controller ) ? $probe_account->controller : null );
                            $ctype = ( is_object( $controller ) && isset( $controller->type ) ? $controller->type : null );
                            // "account" === self-controlled standalone account (potential platform).
                            // "application" === this account is a connected account under another
                            // platform, so keys for it can't list connected accounts. Skip.
                            if ( $ctype !== null && $ctype !== 'account' ) {
                                continue;
                            }
                            $probe_list = $stripe->accounts->all( array(
                                'limit' => SPP_SCA_PER_PAGE,
                            ) );
                            if ( !$probe_list || !is_array( $probe_list->data ) ) {
                                continue;
                            }
                            $accounts = $probe_list;
                            $used_secret_key = $cand['key'];
                            break;
                        } catch ( \Throwable $e ) {
                            $msg = (string) $e->getMessage();
                            if ( $cand['is_oauth_scoped'] && false !== stripos( $msg, "connected accounts of your platform's connected accounts" ) ) {
                                $saw_oauth_failure = true;
                            }
                            continue;
                        }
                    }
                    if ( $accounts !== null && !empty( $used_secret_key ) ) {
                        // 3.7.5: the working candidate key's prefix is the source
                        // of truth for the mode of every account it just listed.
                        $initial_mode = \SplitPay\Core\TransferLogger::resolveStripeMode( $used_secret_key );
                        $account_id = '';
                        foreach ( $accounts->data as $acc ) {
                            $account_id = $acc->id;
                            $account_name = $this->resolve_account_display_name( $acc );
                            $account_email = ( !empty( $acc->email ) ? $acc->email : '' );
                            $wpdb->query( 
                                // Data bound via prepare() placeholders; only the constant table name ($wpdb->prefix . SPP_STRP_ACCNT_TABLE) is concatenated.
                                $wpdb->prepare( 
                                    'insert into ' . $wpdb->prefix . SPP_STRP_ACCNT_TABLE . ' (bsd_account_id, bsd_account_name, bsd_account_email, bsd_account_mode) values (%s, %s, %s, %s)',
                                    // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- constant table identifier; data bound via %s placeholders.
                                    array(
                                        $account_id,
                                        $account_name,
                                        $account_email,
                                        $initial_mode
                                    )
                                 )
                             );
                        }
                        if ( !empty( $account_id ) ) {
                            update_option( 'last_fetched_account', $account_id );
                        }
                        if ( !$accounts->has_more ) {
                            update_option( 'is_all_sca_fetched', true );
                        }
                    } else {
                        // No candidate key worked. If the OAuth-scoped key
                        // returned the specific "can't list connected
                        // accounts" rejection, surface the friendly
                        // actionable notice instead of nothing.
                        if ( $saw_oauth_failure ) {
                            set_transient( 'spp_ifae_notice', __( 'The Stripe key on file (from the WooCommerce Stripe Gateway connection) cannot list connected accounts. Paste your platform secret key from Stripe Dashboard > Developers > API keys into the Live/Test Mode Secret Key field on the Integrations tab.', 'bsd-split-pay-stripe-connect-woo' ), 120 );
                            add_action( 'admin_notices', array($this, 'initial_fetch_acc_exceptions') );
                        }
                    }
                }
            }
        }

        public function initial_fetch_acc_exceptions() {
            $notice = get_transient( 'spp_ifae_notice' );
            if ( !empty( $notice ) ) {
                delete_transient( 'spp_ifae_notice' );
                echo '<div class="notice notice-error is-dismissible">
                    <p><strong>Split Pay for Stripe Connect</strong>: ' . esc_html( $notice ) . '</p>
                </div>';
            }
        }

        /**
         * @since 3.7.5 Optional $mode filter. When 'test' or 'live' is passed,
         *        returns accounts connected in that mode PLUS any legacy /
         *        not-yet-detected rows (bsd_account_mode '' or 'unknown') so
         *        nothing silently disappears before the reconciler tags them.
         *        Null (default) preserves the old "return everything" behaviour
         *        for callers that aren't mode-aware.
         *
         * @param string|null $mode 'test'|'live'|null
         * @return array
         */
        public function get_stored_accounts( $mode = null ) {
            global $wpdb;
            $table = $wpdb->prefix . SPP_STRP_ACCNT_TABLE;
            if ( ($mode === 'test' || $mode === 'live') && $this->account_mode_column_exists() ) {
                $account_results = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$table} WHERE bsd_account_mode = %s OR bsd_account_mode IN ('', 'unknown')", $mode ), ARRAY_A );
            } else {
                $account_results = $wpdb->get_results( "SELECT * FROM {$table}", ARRAY_A );
            }
            return $account_results;
        }

        /**
         * Whether the bsd_account_mode column exists yet (migration-377). Cached
         * per-request. Guards the mode-filtered queries from fataling on the
         * brief window between upgrade and the migration running, or if the
         * ALTER was ever rejected by the host.
         *
         * @since 3.7.5
         * @return bool
         */
        private function account_mode_column_exists() {
            static $exists = null;
            if ( $exists !== null ) {
                return $exists;
            }
            global $wpdb;
            $table = $wpdb->prefix . SPP_STRP_ACCNT_TABLE;
            $exists = (bool) $wpdb->get_var( $wpdb->prepare( 'SELECT COUNT(*) FROM information_schema.COLUMNS
				 WHERE table_schema = DATABASE() AND table_name = %s AND column_name = %s', $table, 'bsd_account_mode' ) );
            return $exists;
        }

        /**
         * The Stripe mode the store is operating in *right now*, for filtering
         * account dropdowns + the mode badge. Uses the canonical
         * TransferLogger::resolveStripeMode() (Payment-Plugins-aware, then WC
         * Stripe) so the dropdowns match what the transfer engine will actually
         * use; falls back to the gateway testmode flag if Core isn't loaded.
         *
         * @since 3.7.5
         * @return string 'test'|'live'
         */
        public function active_stripe_mode() {
            if ( class_exists( '\\SplitPay\\Core\\TransferLogger' ) ) {
                $mode = \SplitPay\Core\TransferLogger::resolveStripeMode();
                if ( $mode === 'test' || $mode === 'live' ) {
                    return $mode;
                }
            }
            return $this->get_stripe_mode();
        }

        /**
         * Prominent, self-contained badge showing the store's active Stripe
         * mode: green "● LIVE MODE" / amber "● TEST MODE". Inline-styled so it
         * renders identically on every admin tab regardless of stylesheet load
         * order. Use near the account dropdowns so a merchant always knows
         * which mode's accounts they're looking at (3.7.5).
         *
         * @since 3.7.5
         * @param string|null $mode Force a mode; defaults to active_stripe_mode().
         * @return string HTML.
         */
        public function mode_badge( $mode = null ) {
            $mode = ( $mode === 'test' || $mode === 'live' ? $mode : $this->active_stripe_mode() );
            $is_live = $mode === 'live';
            $bg = ( $is_live ? '#dff5e6' : '#fff3d4' );
            $fg = ( $is_live ? '#0a7a2f' : '#8a5a00' );
            $dot = ( $is_live ? '#16a34a' : '#d99100' );
            $label = ( $is_live ? esc_html__( 'LIVE MODE', 'bsd-split-pay-stripe-connect-woo' ) : esc_html__( 'TEST MODE', 'bsd-split-pay-stripe-connect-woo' ) );
            $title = ( $is_live ? esc_attr__( 'Split Pay is operating in Stripe LIVE mode. Only live-mode connected accounts can receive transfers.', 'bsd-split-pay-stripe-connect-woo' ) : esc_attr__( 'Split Pay is operating in Stripe TEST mode. Only test-mode connected accounts can receive transfers.', 'bsd-split-pay-stripe-connect-woo' ) );
            return sprintf(
                '<span class="spp-mode-badge spp-mode-badge--%1$s" title="%2$s" style="display:inline-flex;align-items:center;gap:6px;padding:4px 12px;border-radius:999px;background:%3$s;color:%4$s;font-weight:700;font-size:12px;line-height:1.6;letter-spacing:.04em;vertical-align:middle;">' . '<span aria-hidden="true" style="width:9px;height:9px;border-radius:50%%;background:%5$s;display:inline-block;"></span>%6$s</span>',
                esc_attr( $mode ),
                $title,
                $bg,
                $fg,
                $dot,
                $label
            );
        }

        /**
         * Fetch a single cached account row by its Stripe id (incl. its mode).
         *
         * @since 3.7.5
         * @param string $account_id
         * @return array|null
         */
        public function get_account_row_by_id( $account_id ) {
            if ( !is_string( $account_id ) || $account_id === '' ) {
                return null;
            }
            global $wpdb;
            $table = $wpdb->prefix . SPP_STRP_ACCNT_TABLE;
            $row = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$table} WHERE bsd_account_id = %s", $account_id ), ARRAY_A );
            return ( $row ?: null );
        }

        /**
         * Build the display label for one account row.
         *
         * @since 3.7.5
         * @param array  $row   Account row.
         * @param string $style 'full' = "Name - email" (Global Settings tab),
         *                       'name' = "Name" or id (product tables).
         * @return string
         */
        private function compose_account_label( $row, $style = 'full' ) {
            $id = ( isset( $row['bsd_account_id'] ) ? trim( (string) $row['bsd_account_id'] ) : '' );
            $name = ( isset( $row['bsd_account_name'] ) ? (string) $row['bsd_account_name'] : '' );
            $email = ( isset( $row['bsd_account_email'] ) ? (string) $row['bsd_account_email'] : '' );
            $has_name = $name !== '' && $name !== $id;
            $has_email = $email !== '';
            if ( $style === 'name' ) {
                return ( $has_name ? $name : $id );
            }
            // 'full'
            if ( $has_name && $has_email ) {
                return $name . ' - ' . $email;
            }
            if ( $has_name ) {
                return $name;
            }
            if ( $has_email ) {
                return $email;
            }
            return $id;
        }

        /**
         * Human label for a saved account that is NOT in the current
         * mode-filtered dropdown list — distinguishes "wrong Stripe mode" from
         * "not synced". Keeps a saved selection visible + labelled so it never
         * silently disappears (which would let the next save wipe the vendor
         * mapping — the sc-7201 "vendor disappears" data-loss class).
         *
         * @since 3.7.5
         * @param string $saved_id
         * @param string $active_mode 'test'|'live'|''
         * @return string Unescaped label (caller must esc_html()).
         */
        public function unavailable_account_label( $saved_id, $active_mode = '' ) {
            $saved_id = trim( (string) $saved_id );
            $row = $this->get_account_row_by_id( $saved_id );
            $base = ( $row ? $this->compose_account_label( $row, 'name' ) : $saved_id );
            if ( $row && $base !== $saved_id ) {
                $base .= ' (' . $saved_id . ')';
            }
            $acct_mode = ( $row && !empty( $row['bsd_account_mode'] ) ? $row['bsd_account_mode'] : '' );
            if ( in_array( $acct_mode, array('test', 'live'), true ) && in_array( $active_mode, array('test', 'live'), true ) && $acct_mode !== $active_mode ) {
                return sprintf(
                    /* translators: 1: account label, 2: account's Stripe mode, 3: store's active mode */
                    __( '%1$s — %2$s-mode account, unavailable in %3$s mode', 'bsd-split-pay-stripe-connect-woo' ),
                    $base,
                    strtoupper( $acct_mode ),
                    strtoupper( $active_mode )
                );
            }
            return sprintf( 
                /* translators: %s = saved account id/name */
                __( '%s (not in synced accounts — re-sync or re-select)', 'bsd-split-pay-stripe-connect-woo' ),
                $base
             );
        }

        public function fetch_accounts() {
            if ( !current_user_can( 'manage_woocommerce' ) ) {
                // wp_send_json() terminates — `die;` was unreachable.
                wp_send_json( array(
                    'is_finished' => true,
                ) );
            }
            global $wpdb;
            // audit P1.4 — collector arrays for the Stripe-API pagination loop.
            // Init here so the outer-scope reads further down (the
            // new_account_ids payload at line ~2456) always see a value, even
            // when the Stripe API returns zero accounts.
            $get_all_accounts = array();
            $accIndex = 0;
            $page = ( isset( $_POST['page'] ) && !empty( $_POST['page'] ) ? absint( $_POST['page'] ) : 2 );
            $per_page = SPP_SCA_PER_PAGE;
            $offset = $per_page * $page;
            $send_ajax_data = array(
                'is_finished'  => false,
                'current_page' => $page,
                'offset'       => $offset,
                'per_page'     => $per_page,
            );
            if ( isset( $_POST['security'] ) && wp_verify_nonce( wp_unslash( $_POST['security'] ), 'ajax-security' ) ) {
                $woocommerce_stripe_settings = get_option( 'woocommerce_stripe_settings' );
                $data_keys = $this->stp_retrieve_keys();
                $secret_key = $data_keys['secret_key'];
                // 3.7.5: mode of the key paginating this fetch = mode of every
                // account it returns (stamped on insert below).
                $fetch_mode = \SplitPay\Core\TransferLogger::resolveStripeMode( $secret_key );
                if ( !empty( $woocommerce_stripe_settings ) && !empty( $secret_key ) ) {
                    try {
                        $stripe = new \Stripe\StripeClient($secret_key);
                        $last_fetched_account = get_option( 'last_fetched_account', false );
                        $account_args = array(
                            'limit' => $per_page,
                        );
                        if ( $last_fetched_account ) {
                            $account_args['starting_after'] = $last_fetched_account;
                        }
                        $accounts = $stripe->accounts->all( $account_args );
                        if ( $accounts && is_array( $accounts->data ) ) {
                            $accIndex = 0;
                            $get_all_accounts = array();
                            $account_id = '';
                            foreach ( $accounts->data as $acc ) {
                                $account_id = $acc->id;
                                $account_name = $this->resolve_account_display_name( $acc );
                                $account_email = ( !empty( $acc->email ) ? $acc->email : '' );
                                $account_country = ( !empty( $acc->country ) ? $acc->country : '' );
                                if ( !empty( $account_country ) ) {
                                    $countries_map = get_option( 'spp_account_countries', array() );
                                    $countries_map[$account_id] = $account_country;
                                    update_option( 'spp_account_countries', $countries_map );
                                }
                                $row = $wpdb->get_row( 
                                    // Constant table name concatenated; $account_id bound via %s placeholder in prepare().
                                    $wpdb->prepare( 
                                        'select * from ' . $wpdb->prefix . SPP_STRP_ACCNT_TABLE . '  where bsd_account_id = %s',
                                        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- constant table identifier; value bound via %s placeholder.
                                        $account_id
                                     ),
                                    ARRAY_A
                                 );
                                if ( empty( $row ) ) {
                                    $wpdb->query( 
                                        // Data bound via prepare() placeholders; only the constant table name is concatenated.
                                        $wpdb->prepare( 
                                            'insert into ' . $wpdb->prefix . SPP_STRP_ACCNT_TABLE . ' (bsd_account_id, bsd_account_name, bsd_account_email, bsd_account_mode) values (%s, %s, %s, %s)',
                                            // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- constant table identifier; data bound via %s placeholders.
                                            array(
                                                $account_id,
                                                $account_name,
                                                $account_email,
                                                $fetch_mode
                                            )
                                         )
                                     );
                                    $get_all_accounts[$accIndex]['account_id'] = $account_id;
                                    $get_all_accounts[$accIndex]['account_name'] = $account_name;
                                    $get_all_accounts[$accIndex]['account_email'] = $account_email;
                                    ++$accIndex;
                                } else {
                                    if ( $this->maybe_update_account_row(
                                        $row,
                                        $account_id,
                                        $account_name,
                                        $account_email
                                    ) ) {
                                        $get_all_accounts[$accIndex]['account_id'] = $account_id;
                                        $get_all_accounts[$accIndex]['account_name'] = $account_name;
                                        $get_all_accounts[$accIndex]['account_email'] = $account_email;
                                        ++$accIndex;
                                    }
                                }
                            }
                            update_option( 'last_fetched_account', $account_id );
                        }
                        if ( $accounts->has_more ) {
                            $send_ajax_data = array(
                                'is_finished'     => false,
                                'current_page'    => $page + 1,
                                'offset'          => $page * $per_page,
                                'per_page'        => $per_page,
                                'new_account_ids' => $get_all_accounts,
                            );
                        } else {
                            $send_ajax_data = array(
                                'is_finished' => true,
                                'message'     => __( 'All accounts synchronized.', 'bsd-split-pay-stripe-connect-woo' ),
                            );
                            if ( !empty( $get_all_accounts ) ) {
                                $send_ajax_data['new_account_ids'] = $get_all_accounts;
                                update_option( 'is_all_sca_fetched', true );
                            }
                        }
                    } catch ( \Exception $e ) {
                        $send_ajax_data = array(
                            'is_finished' => true,
                            'message'     => $e->getMessage(),
                        );
                    }
                } else {
                    $send_ajax_data = array(
                        'is_finished' => true,
                    );
                }
            }
            // wp_send_json() terminates — `die;` was unreachable.
            wp_send_json( $send_ajax_data );
        }

        public function add_custom_account() {
            if ( !current_user_can( 'manage_woocommerce' ) ) {
                // wp_send_json() terminates — `die;` was unreachable.
                wp_send_json( array(
                    'success' => false,
                ) );
            }
            if ( isset( $_POST['security'] ) && wp_verify_nonce( wp_unslash( $_POST['security'] ), 'ajax-security' ) ) {
                if ( isset( $_POST['account_id'] ) && !empty( $_POST['account_id'] ) ) {
                    $account_id = sanitize_text_field( wp_unslash( $_POST['account_id'] ) );
                    // 3.7.5: a manually-entered connected account must be a Stripe Connect
                    // account id ('acct_…'). Reject anything else up front so a typo/paste
                    // error can't seed a junk cache row that then fails every transfer.
                    if ( strpos( $account_id, 'acct_' ) !== 0 ) {
                        wp_send_json( array(
                            'success' => false,
                            'message' => esc_html__( 'Please enter a valid Stripe Connect account ID — it should start with "acct_".', 'bsd-split-pay-stripe-connect-woo' ),
                        ) );
                    }
                    global $wpdb;
                    $account_name = $account_id;
                    $account_email = '';
                    $data_keys = $this->stp_retrieve_keys();
                    if ( $data_keys && !empty( $data_keys['secret_key'] ) ) {
                        try {
                            $stripe = new \Stripe\StripeClient($data_keys['secret_key']);
                            $acc = $stripe->accounts->retrieve( $account_id );
                            $account_name = $this->resolve_account_display_name( $acc );
                            $account_email = ( !empty( $acc->email ) ? $acc->email : '' );
                        } catch ( \Exception $e ) {
                            \BSD_Split_Pay_Stripe_Connect_Woo\spp_safe_wcs_log( 'SPP: add_custom_account retrieve failed: ' . $e->getMessage() );
                        }
                    }
                    $row = $wpdb->get_row( 
                        // Constant table name concatenated; $account_id bound via %s placeholder in prepare().
                        $wpdb->prepare( 
                            'select * from ' . $wpdb->prefix . SPP_STRP_ACCNT_TABLE . '  where bsd_account_id = %s',
                            // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- constant table identifier; value bound via %s placeholder.
                            $account_id
                         ),
                        ARRAY_A
                     );
                    if ( empty( $row ) ) {
                        // 3.7.5: stamp the mode the admin is currently operating in
                        // (same key source this manual add resolved through).
                        $custom_mode = $this->current_connect_mode();
                        $wpdb->query( 
                            // Data bound via prepare() placeholders; only the constant table name is concatenated.
                            $wpdb->prepare( 
                                'insert into ' . $wpdb->prefix . SPP_STRP_ACCNT_TABLE . ' (bsd_account_id, bsd_account_name, bsd_account_email, bsd_account_mode) values (%s, %s, %s, %s)',
                                // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- constant table identifier; data bound via %s placeholders.
                                array(
                                    $account_id,
                                    $account_name,
                                    $account_email,
                                    $custom_mode
                                )
                             )
                         );
                    } else {
                        $this->maybe_update_account_row(
                            $row,
                            $account_id,
                            $account_name,
                            $account_email
                        );
                    }
                    // wp_send_json() terminates — `die;` was unreachable.
                    wp_send_json( array(
                        'success'       => true,
                        'account_name'  => $account_name,
                        'account_email' => $account_email,
                    ) );
                }
            }
        }

        public function clear_accounts() {
            // PT-fix-3.7.5 (security #1A): fully reworked. Previous version:
            //   1. Returned success:true even when nonce failed and DELETE did nothing
            //      — Integrations-tab button silently never worked for merchants
            //   2. Accepted only 'ajax-security' nonce (no 'spp_clear_accounts'),
            //      so the actual "Clear Synced Accounts" UI button never matched
            //   3. The `delete from {table}` had no WHERE clause — any caller
            //      who knew the legacy nonce mass-wiped the connected-accounts
            //      table for the whole site
            // New behaviour: accept EITHER the legacy 'ajax-security' nonce OR the
            // modern 'spp_clear_accounts' nonce (UI button uses the modern one;
            // retain legacy for back-compat with old AJAX callers). Return
            // success only after the DELETE has been issued. Use wp_send_json_error
            // for non-OK paths so the JS can react.
            if ( !current_user_can( 'manage_woocommerce' ) ) {
                wp_send_json_error( array(
                    'reason' => 'insufficient_caps',
                ), 403 );
            }
            $nonce = ( isset( $_POST['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_POST['_wpnonce'] ) ) : '' );
            if ( empty( $nonce ) && isset( $_POST['security'] ) ) {
                $nonce = sanitize_text_field( wp_unslash( $_POST['security'] ) );
            }
            $nonce_valid = $nonce && (wp_verify_nonce( $nonce, 'spp_clear_accounts' ) || wp_verify_nonce( $nonce, 'ajax-security' ));
            if ( !$nonce_valid ) {
                wp_send_json_error( array(
                    'reason' => 'invalid_nonce',
                ), 403 );
            }
            global $wpdb;
            // `bsd_strp_accnt` is a per-site cache of Stripe accounts. Truncating
            // the whole table IS the intended semantics of "Clear Synced Accounts".
            // Documented here so a future reviewer doesn't add a phantom WHERE.
            $deleted = $wpdb->query( 
                // Constant table name ($wpdb->prefix . SPP_STRP_ACCNT_TABLE); unconditional DELETE (clear cache) has no bindable parameters.
                'DELETE FROM ' . $wpdb->prefix . SPP_STRP_ACCNT_TABLE
             );
            delete_option( 'last_fetched_account' );
            delete_option( 'is_all_sca_fetched' );
            delete_option( 'spp_account_countries' );
            delete_option( 'spp_platform_country' );
            wp_send_json_success( array(
                'deleted_rows' => (int) $deleted,
            ) );
        }

        public function create_table() {
            global $wpdb, $bsd_stripe_account_tbl_ver;
            $bsd_stripe_account_tbl_installed_ver = get_option( 'bsd_stripe_account_tbl_ver' );
            if ( $bsd_stripe_account_tbl_installed_ver != $bsd_stripe_account_tbl_ver ) {
                $charset_collate = $wpdb->get_charset_collate();
                $sql = 'CREATE TABLE IF NOT EXISTS ' . $wpdb->prefix . SPP_STRP_ACCNT_TABLE . " (\n\t\t\t\t\tbsd_sat_id bigint(20) NOT NULL AUTO_INCREMENT,\n\t\t\t\t\tbsd_account_id varchar(55) DEFAULT '' NOT NULL,\n\t\t\t\t\tbsd_account_name varchar(55) DEFAULT '' NOT NULL,\n\t\t\t\t\tbsd_account_mode varchar(10) DEFAULT '' NOT NULL,\n\t\t\t\t\tPRIMARY KEY  (bsd_sat_id)\n\t\t\t\t) {$charset_collate};";
                require_once ABSPATH . 'wp-admin/includes/upgrade.php';
                dbDelta( $sql );
                add_option( 'bsd_stripe_account_tbl_ver', $bsd_stripe_account_tbl_ver );
                $bsd_split_pay_stripe_connect_woo_stripe_connected_account = get_option( 'bsd_split_pay_stripe_connect_woo_stripe_connected_account' );
                // 3.7.5: stamp the legacy seed account with the current mode.
                $seed_mode = $this->current_connect_mode();
                $wpdb->query( 
                    // Data bound via prepare() placeholders; only the constant table name ($wpdb->prefix . SPP_STRP_ACCNT_TABLE) is concatenated.
                    $wpdb->prepare( 
                        'insert into ' . $wpdb->prefix . SPP_STRP_ACCNT_TABLE . ' (bsd_account_id, bsd_account_mode) values (%s, %s)',
                        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- constant table identifier; data bound via %s placeholders.
                        array($bsd_split_pay_stripe_connect_woo_stripe_connected_account, $seed_mode)
                     )
                 );
            }
        }

        public function is_stripe_enabled_and_configured() {
            $enabled_configured = true;
            $woocommerce_stripe_settings = get_option( 'woocommerce_stripe_settings' );
            if ( isset( $woocommerce_stripe_settings['enabled'] ) && $woocommerce_stripe_settings['enabled'] == 'yes' ) {
                $testmode = '';
                if ( isset( $woocommerce_stripe_settings['testmode'] ) ) {
                    $testmode = $woocommerce_stripe_settings['testmode'];
                }
                /*
                 * 3.7.5 (sc-7242): customers who save Stripe keys via the new
                 * Integrations tab UI write to the per-gateway option names
                 * (`spp_wc_stripe_test_secret_key` / `spp_wc_stripe_live_secret_key`).
                 * The legacy `stripe_*_api_*_key` options that this gate
                 * previously read are only populated by the older Stripe
                 * Configuration tab. We now check BOTH locations — a key in
                 * either path counts as "configured" — so the Main tab no
                 * longer stays grayed out for customers who used the new UI
                 * (e.g. Margaret Winans, flameoflove.us, ticket-launch weekend).
                 */
                $stripe_test_api_public_key = get_option( 'stripe_test_api_public_key', false );
                $stripe_test_api_secret_key = get_option( 'stripe_test_api_secret_key', false );
                $stripe_api_public_key = get_option( 'stripe_api_public_key', false );
                $stripe_api_secret_key = get_option( 'stripe_api_secret_key', false );
                // Per-gateway fallback secret keys saved via the Integrations tab.
                if ( empty( $stripe_test_api_secret_key ) ) {
                    $stripe_test_api_secret_key = get_option( 'spp_wc_stripe_test_secret_key', false );
                }
                if ( empty( $stripe_api_secret_key ) ) {
                    $stripe_api_secret_key = get_option( 'spp_wc_stripe_live_secret_key', false );
                }
                if ( $testmode == 'yes' ) {
                    if ( empty( $stripe_test_api_public_key ) && empty( $stripe_test_api_secret_key ) ) {
                        $enabled_configured = false;
                    }
                } elseif ( $testmode == 'no' ) {
                    if ( empty( $stripe_api_public_key ) && empty( $stripe_api_secret_key ) ) {
                        $enabled_configured = false;
                    }
                }
            }
            return $enabled_configured;
        }

    }

}