<?php
/**
 * PayStarSea Webhook Handler
 * 
 * Handles webhook callbacks from PayStarSea platform
 * Includes signature verification and idempotency checks
 */

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

class SeaXPay_Webhook {
    
    public function __construct() {
        add_action('woocommerce_api_seaxpay_webhook', array($this, 'handle_webhook'));
    }
    
    public function handle_webhook() {
        global $wpdb;
        
        $raw_body = file_get_contents('php://input'); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- reading raw POST body for webhook signature verification; no WP alternative exists.
        
        $signature = isset($_SERVER['HTTP_X_SIGNATURE']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_X_SIGNATURE'])) : '';
        $timestamp = isset($_SERVER['HTTP_X_TIMESTAMP']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_X_TIMESTAMP'])) : 0;
        
        seaxpay_log('webhook', 'Webhook received', null, null, 
            array('signature' => substr($signature, 0, 16) . '...', 'timestamp' => $timestamp), null);
        
        if (empty($signature) || empty($timestamp)) {
            seaxpay_log('error', 'Webhook: Missing signature or timestamp', null, null, null, null);
            $this->send_error_response(401, 'Missing signature or timestamp');
            return;
        }
        
    
        $current_time = time() * 1000;
        if (abs($current_time - $timestamp) > 300000) {
            seaxpay_log('error', 'Webhook: Request expired (>5min)', null, null,
                array('current_time' => $current_time, 'timestamp' => $timestamp), null);
            $this->send_error_response(401, 'Request expired');
            return;
        }

        
        $nonce_key = 'seaxpay_wh_nonce_' . substr($signature, 0, 40);
        if (get_transient($nonce_key) !== false) {
            seaxpay_log('webhook', 'Webhook: idempotent replay (nonce hit) -> return 200', null, null,
                array('sig_prefix' => substr($signature, 0, 16) . '...', 'timestamp' => $timestamp), null);
            $this->send_success_response('Already processed');
            return;
        }

        $config = seaxpay_get_config();

        if (empty($config) || empty($config->webhook_secret)) {
            seaxpay_log('error', 'Webhook: Secret not configured', null, null, null, null);
            $this->send_error_response(500, 'Webhook secret not configured');
            return;
        }

        $calculated_signature = hash_hmac('sha256', $raw_body, $config->webhook_secret);

        if (!hash_equals($calculated_signature, $signature)) {
            seaxpay_log('error', 'Webhook: Signature verification failed', null, null, null, null);
            $this->send_error_response(401, 'Invalid signature');
            return;
        }
        
        $data = json_decode($raw_body, true, 512, JSON_BIGINT_AS_STRING);
        
        if (json_last_error() !== JSON_ERROR_NONE) {
            seaxpay_log('error', 'Webhook: Invalid JSON', null, null, null, array(
                'body_length' => is_string($raw_body) ? strlen($raw_body) : 0,
            ));
            $this->send_error_response(400, 'Invalid JSON');
            return;
        }

        $data = $this->sanitize_webhook_payload($data);

        $platform_order_no = isset($data['order_no']) ? sanitize_text_field($data['order_no']) : '';
        
        if (empty($platform_order_no)) {
            seaxpay_log('error', 'Webhook: Missing order_no', null, null, $data, null);
            $this->send_error_response(400, 'Missing order_no');
            return;
        }
        
        $existing_order = seaxpay_get_order_by_platform_no($platform_order_no);
        
        if (!$existing_order) {
            seaxpay_log('error', 'Webhook: Order not found', null, $platform_order_no, $data, null);
            $this->send_error_response(404, 'Order not found');
            return;
        }
        
        $event_name = isset($data['event']) ? sanitize_text_field($data['event']) : '';
        $post_paid_events = array(
            'merchant.withdraw_submitted',
            'merchant.withdraw_completed',
            'merchant.withdraw_failed',
            'merchant.withdraw_review_timeout',
            'merchant.withdraw_review_approved',
            'merchant.withdraw_review_rejected',
            'merchant.withdraw_review_frozen',
            'payment.pending_withdraw',
            'payment.pending_withdraw_expire_warn',
            'payment.risk_blocked',
            'payment.risk_approved',
            'payment.risk_rejected',
            'payment.risk_refund_initiated',
            'payment.risk_refunded',
            'payment.risk_frozen',
            'payment.overpaid',
            'payment.auto_expire_withdraw_triggered',
            'payment.pending_withdraw_no_wallet',
            'payment.settle_delayed'
        );
        $is_post_paid_event = in_array($event_name, $post_paid_events, true);

        if ($existing_order->status === 'paid' && !$is_post_paid_event) {
            seaxpay_log('webhook', 'Webhook: Order already processed (idempotency)',
                $existing_order->wc_order_id, $platform_order_no, null, null);
            set_transient($nonce_key, 1, 30 * 60);
            $this->send_success_response('Already processed');
            return;
        }

        $incoming_seq_raw = isset($data['event_sequence']) ? (string) $data['event_sequence'] : '0';
        $incoming_seq = preg_match('/^\d+$/', $incoming_seq_raw) ? $incoming_seq_raw : '0';
        $wc_order_id_for_seq = isset($existing_order->wc_order_id) ? (int) $existing_order->wc_order_id : 0;
        $last_seq = '0';
        $seq_wc_order = ($wc_order_id_for_seq > 0) ? wc_get_order($wc_order_id_for_seq) : null;
        if ($incoming_seq !== '0' && $seq_wc_order) {
            $last_seq_raw = (string) $seq_wc_order->get_meta('_seaxpay_last_event_seq', true);
            $last_seq = preg_match('/^\d+$/', $last_seq_raw) ? $last_seq_raw : '0';
            if ($last_seq !== '0' && $this->seq_compare($incoming_seq, $last_seq) < 0) {
                $allow_late_completed = $event_name === 'merchant.withdraw_completed'
                    && in_array($existing_order->status, array('paid', 'withdrawing', 'pending_withdraw'), true);
                if ($allow_late_completed) {
                    seaxpay_log('webhook', sprintf(
                        'Webhook: late merchant.withdraw_completed accepted (seq=%s < last=%s, current=%s)',
                        $incoming_seq, $last_seq, $existing_order->status
                    ), $wc_order_id_for_seq, $platform_order_no, seaxpay_sanitize_for_log($data), null);
                } else {
                    seaxpay_log('webhook', sprintf(
                        'Webhook: out-of-order event dropped (seq=%s < last=%s, event=%s)',
                        $incoming_seq, $last_seq, $event_name
                    ), $wc_order_id_for_seq, $platform_order_no, seaxpay_sanitize_for_log($data), null);
                    set_transient($nonce_key, 1, 30 * 60);
                    $this->send_success_response('Out-of-order event dropped');
                    return;
                }
            }
        }

        $this->process_order_update($data, $config, $existing_order);

        
        if ($incoming_seq !== '0' && $seq_wc_order
            && ($last_seq === '0' || $this->seq_compare($incoming_seq, $last_seq) > 0)) {
            $seq_wc_order->update_meta_data('_seaxpay_last_event_seq', $incoming_seq);
            $seq_wc_order->save();
        }

        
        set_transient($nonce_key, 1, 30 * 60);
        $this->send_success_response('Success');
    }
    
    private function process_order_update($data, $config, $existing_order) {
        global $wpdb;
        
        $event = $data['event'] ?? '';
        $status = $data['status'] ?? '';
        $platform_order_no = $data['order_no'];
        $wc_order_id = $existing_order->wc_order_id;
        
        seaxpay_log('webhook', "Processing order update: event=$event, status=$status", 
            $wc_order_id, $platform_order_no, seaxpay_sanitize_for_log($data), null);
        
        if ($event === 'payment.success' && $status === 'paid') {
            $no_downgrade_statuses = array('completed', 'settled', 'risk_refunded', 'risk_frozen');
            if (in_array($existing_order->status, $no_downgrade_statuses, true)) {
                seaxpay_log('webhook', sprintf(
                    'payment.success ignored: current status=%s is terminal, refuse downgrade',
                    $existing_order->status
                ), $wc_order_id, $platform_order_no, seaxpay_sanitize_for_log($data), null);
                return;
            }
            $actual_amount_str = isset($data['actual_amount']) ? strval($data['actual_amount']) : '0';
            $expected_amount_str = strval($existing_order->crypto_amount);
            
            $actual_amount = $actual_amount_str;
            $expected_amount = $expected_amount_str;
            
            if (bccomp($actual_amount, '0', 18) <= 0) {
                seaxpay_log('error', 'Webhook missing actual_amount field', 
                    $wc_order_id, $platform_order_no, seaxpay_sanitize_for_log($data), null);
                $this->send_error_response(400, 'Missing actual_amount field');
                return;
            }
            
            $tolerance_rate = '0.0001';
            $min_acceptable = bcmul($expected_amount, bcsub('1', $tolerance_rate, 18), 18);
            
            if (bccomp($actual_amount, $min_acceptable, 18) < 0) {
                seaxpay_log('error', sprintf(
                    'Payment amount insufficient: expected=%s, actual=%s, min_acceptable=%s',
                    $expected_amount, $actual_amount, $min_acceptable
                ), $wc_order_id, $platform_order_no, seaxpay_sanitize_for_log($data), null);
                
                $wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
                    "{$wpdb->prefix}seaxpay_orders",
                    array(
                        'status' => 'failed',
                        'tx_hash' => $data['tx_hash'] ?? null,
                        'platform_callback_data' => json_encode($data)
                    ),
                    array('platform_order_no' => $platform_order_no)
                );
                
                $wc_order = wc_get_order($wc_order_id);
                if ($wc_order) {
                    $wc_order->update_status('failed', sprintf(
                        /* translators: %1$s: expected crypto amount, %2$s: received crypto amount */
                        __('Payment amount insufficient. Expected: %1$s, Received: %2$s', 'paystarsea-crypto-payment-gateway'),
                        $expected_amount, $actual_amount
                    ));
                }
                
                $this->send_error_response(400, 'Payment amount insufficient');
                return;
            }
            
            seaxpay_log('webhook', sprintf(
                'Payment amount verified: expected=%s, actual=%s, tolerance=%s%%',
                $expected_amount, $actual_amount, ($tolerance_rate * 100)
            ), $wc_order_id, $platform_order_no, null, null);
            
            $update_data = array(
                'status' => 'paid',
                'customer_address' => $data['customer_address'] ?? null,
                'tx_hash' => $data['tx_hash'] ?? null,
                'paid_time' => isset($data['paid_time']) ? gmdate('Y-m-d H:i:s', strtotime($data['paid_time'])) : gmdate('Y-m-d H:i:s'),
                'platform_callback_data' => json_encode($data)
            );
            
            $wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
                "{$wpdb->prefix}seaxpay_orders",
                $update_data,
                array('platform_order_no' => $platform_order_no)
            );
            
            $wc_order = wc_get_order($wc_order_id);
            
            if ($wc_order) {
                $wc_order->add_order_note(
                    sprintf(
                        /* translators: %1$s: actual paid amount, %2$s: expected amount, %3$s: blockchain transaction hash */
                        __('Payment received via SeaXPay. Amount: %1$s (Expected: %2$s). Transaction Hash: %3$s', 'paystarsea-crypto-payment-gateway'),
                        $actual_amount,
                        $expected_amount,
                        $data['tx_hash'] ?? 'N/A'
                    )
                );
                
                $wc_order->payment_complete($data['tx_hash'] ?? '');
                
                if ($config->auto_complete_order == 1) {
                    $wc_order->update_status('completed', __('Order auto-completed after payment confirmation.', 'paystarsea-crypto-payment-gateway'));
                }
                
                seaxpay_log('webhook', 'Order payment completed successfully', 
                    $wc_order_id, $platform_order_no, null, null);
            } else {
                seaxpay_log('error', 'WooCommerce order not found', 
                    $wc_order_id, $platform_order_no, null, null);
            }
            
        } elseif ($event === 'payment.confirming' || $status === 'confirming') {
            $event_for_skip = 'payment.confirming';
            $skip_via_ssot = function_exists('seaxpay_should_skip_event')
                && seaxpay_should_skip_event($event_for_skip, $existing_order->status);
            if ($skip_via_ssot) {
                seaxpay_log('webhook', sprintf(
                    'Confirming event skipped (SSoT): current_status=%s',
                    $existing_order->status
                ), $wc_order_id, $platform_order_no, null, null);
                return;
            }
            $skip_statuses = array('confirming', 'paid', 'settled', 'risk_blocked', 'risk_refunded', 'risk_frozen');
            if (in_array($existing_order->status, $skip_statuses, true)) {
                seaxpay_log('webhook', sprintf(
                    'Confirming event skipped (legacy): current_status=%s',
                    $existing_order->status
                ), $wc_order_id, $platform_order_no, null, null);
                return;
            }

            $wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
                "{$wpdb->prefix}seaxpay_orders",
                array(
                    'status' => 'confirming',
                    'tx_hash' => $data['tx_hash'] ?? null,
                    'paid_time' => isset($data['paid_time']) ? gmdate('Y-m-d H:i:s', strtotime($data['paid_time'])) : gmdate('Y-m-d H:i:s'),
                    'platform_callback_data' => json_encode($data)
                ),
                array('platform_order_no' => $platform_order_no)
            );
            
            $wc_order = wc_get_order($wc_order_id);
            if ($wc_order) {
                $wc_order->add_order_note(
                    sprintf(
                        /* translators: %s: blockchain transaction hash */
                        __('Payment detected, awaiting blockchain confirmations. Transaction Hash: %s', 'paystarsea-crypto-payment-gateway'),
                        $data['tx_hash'] ?? 'N/A'
                    )
                );
            }
            
            seaxpay_log('webhook', 'Order confirming - payment detected', 
                $wc_order_id, $platform_order_no, seaxpay_sanitize_for_log($data), null);
            
        } elseif ($event === 'payment.settle_delayed') {
            $current_tx_hash = $data['tx_hash'] ?? 'N/A';

            $wc_order = wc_get_order($wc_order_id);
            if ($wc_order) {
                $wc_order->add_order_note(sprintf(
                    /* translators: %s: blockchain transaction hash */
                    __('Gateway settlement is delayed for this paid order; platform support will retry automatically. Transaction hash: %s. No action required from buyer; merchant ops may contact platform if delay exceeds business SLA.', 'paystarsea-crypto-payment-gateway'),
                    $current_tx_hash
                ));
            }

            seaxpay_log('webhook', sprintf('Settle delayed alert: tx=%s', $current_tx_hash),
                $wc_order_id, $platform_order_no, seaxpay_sanitize_for_log($data), null);

        } elseif ($event === 'payment.confirming_timeout_alert') {
            $soft_hours = $data['soft_timeout_hours'] ?? 'N/A';
            $hard_hours = $data['hard_fail_hours'] ?? 'N/A';
            $current_tx_hash = $data['tx_hash'] ?? 'N/A';

            $wc_order = wc_get_order($wc_order_id);
            if ($wc_order) {
                $wc_order->add_order_note(sprintf(
                    /* translators: %1$s: soft timeout hours, %2$s: hard fail hours, %3$s: transaction hash */
                    __('Payment confirmation is taking longer than expected (>%1$s hours). Gateway is still monitoring the transaction; it will be auto-failed after %2$s hours if not confirmed. Transaction hash: %3$s', 'paystarsea-crypto-payment-gateway'),
                    $soft_hours, $hard_hours, $current_tx_hash
                ));
            }

            seaxpay_log('webhook', sprintf('Confirming timeout alert: softHours=%s, hardHours=%s, tx=%s', $soft_hours, $hard_hours, $current_tx_hash),
                $wc_order_id, $platform_order_no, seaxpay_sanitize_for_log($data), null);

        } elseif ($event === 'payment.expired' || $status === 'expired') {
            $skip_via_ssot = function_exists('seaxpay_should_skip_event')
                && seaxpay_should_skip_event('payment.expired', $existing_order->status);
            if ($skip_via_ssot) {
                seaxpay_log('webhook', sprintf(
                    'payment.expired skipped (SSoT): current=%s',
                    $existing_order->status
                ), $wc_order_id, $platform_order_no, seaxpay_sanitize_for_log($data), null);
                return;
            }
            $skip_statuses_for_expired = array(
                'paid', 'settled', 'confirming',
                'risk_blocked', 'risk_refunded', 'risk_frozen',
                'expired_refund_pending',
            );
            if (in_array($existing_order->status, $skip_statuses_for_expired, true)) {
                seaxpay_log('webhook', sprintf(
                    'payment.expired skipped (legacy): current=%s',
                    $existing_order->status
                ), $wc_order_id, $platform_order_no, seaxpay_sanitize_for_log($data), null);
                return;
            }

            seaxpay_update_order_status($platform_order_no, 'expired');

            $wc_order = wc_get_order($wc_order_id);
            if ($wc_order && $wc_order->get_status() === 'pending') {
                $wc_order->update_status('cancelled', __('Payment expired.', 'paystarsea-crypto-payment-gateway'));
            }

            seaxpay_log('webhook', 'Order expired', $wc_order_id, $platform_order_no, null, null);

        } elseif ($event === 'payment.expired_refund_pending' || $status === 'expired_refund_pending') {
            $aggregated = $data['actual_amount'] ?? '0';
            $payer_address = $data['payer_address'] ?? '';
            $tx_hash = $data['tx_hash'] ?? '';
            $risk_reason = $data['risk_reason'] ?? '';

            $wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
                "{$wpdb->prefix}seaxpay_orders",
                array(
                    'status' => 'expired_refund_pending',
                    'customer_address' => $payer_address !== '' ? $payer_address : null,
                    'tx_hash' => $tx_hash !== '' ? $tx_hash : null,
                    'platform_callback_data' => wp_json_encode($data)
                ),
                array('platform_order_no' => $platform_order_no)
            );

            seaxpay_log('webhook', sprintf(
                'Order expired with partial inbound (silent): aggregated=%s, payer=%s, tx=%s, reason=%s',
                $aggregated, $payer_address, $tx_hash, $risk_reason
            ), $wc_order_id, $platform_order_no, seaxpay_sanitize_for_log($data), null);

        } elseif ($event === 'payment.failed' || $status === 'failed') {
            seaxpay_update_order_status($platform_order_no, 'failed');
            
            $wc_order = wc_get_order($wc_order_id);
            if ($wc_order) {
                $wc_order->update_status('failed', __('Payment failed.', 'paystarsea-crypto-payment-gateway'));
            }
            
            seaxpay_log('webhook', 'Order failed', $wc_order_id, $platform_order_no, null, null);
            
        } elseif ($event === 'payment.risk_blocked' || $status === 'risk_blocked') {
            $risk_reason = $data['risk_reason'] ?? 'Unknown risk';
            $payer_address = $data['payer_address'] ?? 'N/A';
            
            seaxpay_update_order_status($platform_order_no, 'risk_blocked');
            
            $wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
                "{$wpdb->prefix}seaxpay_orders",
                array(
                    'tx_hash' => $data['tx_hash'] ?? null,
                    'platform_callback_data' => json_encode($data)
                ),
                array('platform_order_no' => $platform_order_no)
            );
            
            $wc_order = wc_get_order($wc_order_id);
            if ($wc_order) {
                $wc_order->update_status('on-hold', sprintf(
                    /* translators: %1$s: risk reason, %2$s: payer wallet address */
                    __('Payment blocked by risk control. Reason: %1$s. Payer address: %2$s. Pending manual review.', 'paystarsea-crypto-payment-gateway'),
                    $risk_reason, $payer_address
                ));
                
                $wc_order->add_order_note(sprintf(
                    /* translators: %s: blockchain transaction hash */
                    __('Risk control alert: This payment has been flagged and is under review. Transaction hash: %s', 'paystarsea-crypto-payment-gateway'),
                    $data['tx_hash'] ?? 'N/A'
                ));
            }
            
            seaxpay_log('webhook', sprintf('Order risk blocked: reason=%s, payer=%s', $risk_reason, $payer_address), 
                $wc_order_id, $platform_order_no, seaxpay_sanitize_for_log($data), null);
            
        } elseif ($event === 'payment.risk_approved' || $status === 'risk_approved') {
            $actual_amount_str = isset($data['actual_amount']) ? strval($data['actual_amount']) : '0';
            $expected_amount_str = strval($existing_order->crypto_amount);
            
            $actual_amount = $actual_amount_str;
            $expected_amount = $expected_amount_str;
            
            if (bccomp($actual_amount, '0', 18) <= 0) {
                seaxpay_log('error', 'Risk approved webhook missing actual_amount field', 
                    $wc_order_id, $platform_order_no, seaxpay_sanitize_for_log($data), null);
                $this->send_error_response(400, 'Missing actual_amount field');
                return;
            }
            
            $tolerance_rate = '0.0001';
            $min_acceptable = bcmul($expected_amount, bcsub('1', $tolerance_rate, 18), 18);
            
            if (bccomp($actual_amount, $min_acceptable, 18) < 0) {
                seaxpay_log('error', sprintf(
                    'Risk approved amount insufficient: expected=%s, actual=%s, min_acceptable=%s',
                    $expected_amount, $actual_amount, $min_acceptable
                ), $wc_order_id, $platform_order_no, seaxpay_sanitize_for_log($data), null);
                
                $wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
                    "{$wpdb->prefix}seaxpay_orders",
                    array(
                        'status' => 'failed',
                        'tx_hash' => $data['tx_hash'] ?? null,
                        'platform_callback_data' => json_encode($data)
                    ),
                    array('platform_order_no' => $platform_order_no)
                );
                
                $wc_order = wc_get_order($wc_order_id);
                if ($wc_order) {
                    $wc_order->update_status('failed', sprintf(
                        /* translators: %1$s: expected crypto amount, %2$s: received crypto amount */
                        __('Risk approved but amount insufficient. Expected: %1$s, Received: %2$s', 'paystarsea-crypto-payment-gateway'),
                        $expected_amount, $actual_amount
                    ));
                }
                
                $this->send_error_response(400, 'Payment amount insufficient');
                return;
            }
            
            seaxpay_log('webhook', sprintf(
                'Risk approved amount verified: expected=%s, actual=%s',
                $expected_amount, $actual_amount
            ), $wc_order_id, $platform_order_no, null, null);
            
            $settlement_mode = isset($data['settlement_mode']) ? strtolower(trim(strval($data['settlement_mode']))) : 'auto';
            $seaxpay_status = ($settlement_mode === 'manual') ? 'pending_withdraw' : 'paid';
            $update_data = array(
                'status' => $seaxpay_status,
                'customer_address' => $data['customer_address'] ?? null,
                'tx_hash' => $data['tx_hash'] ?? null,
                'paid_time' => isset($data['paid_time']) ? gmdate('Y-m-d H:i:s', strtotime($data['paid_time'])) : gmdate('Y-m-d H:i:s'),
                'platform_callback_data' => json_encode($data)
            );

            $wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
                "{$wpdb->prefix}seaxpay_orders",
                $update_data,
                array('platform_order_no' => $platform_order_no)
            );

            $wc_order = wc_get_order($wc_order_id);
            if ($wc_order) {
                $wc_order->add_order_note(
                    sprintf(
                        /* translators: %1$s: actual amount, %2$s: expected amount, %3$s: settlement mode, %4$s: transaction hash */
                        __('Risk review passed. Payment approved by admin. Amount: %1$s (Expected: %2$s). Settlement: %3$s. Transaction Hash: %4$s', 'paystarsea-crypto-payment-gateway'),
                        $actual_amount_str,
                        $expected_amount_str,
                        $settlement_mode,
                        $data['tx_hash'] ?? 'N/A'
                    )
                );

                if ($settlement_mode === 'manual') {
                    if ($wc_order->get_status() !== 'on-hold' && $wc_order->get_status() !== 'completed') {
                        $wc_order->update_status('on-hold', sprintf(
                            /* translators: %s: blockchain transaction hash */
                            __('Risk approved (manual settlement). Funds remain at derived address awaiting merchant withdrawal. Transaction hash: %s', 'paystarsea-crypto-payment-gateway'),
                            $data['tx_hash'] ?? 'N/A'
                        ));
                    }
                } else {
                    $wc_order->payment_complete($data['tx_hash'] ?? '');
                    if ($config->auto_complete_order == 1) {
                        $wc_order->update_status('completed', __('Order auto-completed after risk approval.', 'paystarsea-crypto-payment-gateway'));
                    }
                }
            }

            seaxpay_log('webhook', sprintf('Order risk approved: amount=%s, expected=%s, settlement_mode=%s, seaxpay_status=%s',
                $actual_amount_str, $expected_amount_str, $settlement_mode, $seaxpay_status),
                $wc_order_id, $platform_order_no, seaxpay_sanitize_for_log($data), null);
            
        } elseif ($event === 'payment.risk_rejected' || $status === 'risk_rejected') {
            if ($existing_order && $existing_order->status === 'risk_frozen') {
                seaxpay_log('webhook', 'risk_rejected suppressed: order already risk_frozen (legacy-compat duplicate)',
                    $wc_order_id, $platform_order_no, seaxpay_sanitize_for_log($data), null);
                return;
            }

            $risk_reason = $data['risk_reason'] ?? 'Unknown';
            $payer_address = $data['payer_address'] ?? 'N/A';

            seaxpay_update_order_status($platform_order_no, 'risk_rejected');

            $wc_order = wc_get_order($wc_order_id);
            if ($wc_order) {
                $wc_order->update_status('cancelled', sprintf(
                    /* translators: %1$s: risk reason, %2$s: payer wallet address */
                    __('Payment permanently rejected by risk control. Reason: %1$s. Payer address: %2$s. Funds are frozen and will not be settled.', 'paystarsea-crypto-payment-gateway'),
                    $risk_reason, $payer_address
                ));

                $wc_order->add_order_note(sprintf(
                    /* translators: %s: blockchain transaction hash */
                    __('Risk review completed: REJECTED. This order has been permanently frozen by the platform. Please contact support for further assistance. Transaction hash: %s', 'paystarsea-crypto-payment-gateway'),
                    $data['tx_hash'] ?? 'N/A'
                ));
            }

            seaxpay_log('webhook', sprintf('Order risk rejected: reason=%s, payer=%s', $risk_reason, $payer_address),
                $wc_order_id, $platform_order_no, seaxpay_sanitize_for_log($data), null);
            
        } elseif ($event === 'payment.risk_refund_initiated' || $status === 'risk_refund_initiated') {
            $risk_reason = $data['risk_reason'] ?? 'Unknown';
            $payer_address = $data['payer_address'] ?? 'N/A';
            $estimated_refund = $data['estimated_refund_amount'] ?? '0';

            $wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
                "{$wpdb->prefix}seaxpay_orders",
                array(
                    'platform_callback_data' => wp_json_encode($data)
                ),
                array('platform_order_no' => $platform_order_no)
            );

            $wc_order = wc_get_order($wc_order_id);
            if ($wc_order) {
                $wc_order->add_order_note(sprintf(
                    /* translators: %1$s: estimated refund amount, %2$s: payer wallet address, %3$s: risk reason */
                    __('Risk review completed: REFUND APPROVED. The platform has approved the refund and is processing the on-chain refund transaction. Estimated refund amount: %1$s. Payer address: %2$s. Reason: %3$s. The order status will be updated to refunded once the on-chain transaction is confirmed.', 'paystarsea-crypto-payment-gateway'),
                    $estimated_refund, $payer_address, $risk_reason
                ));
            }

            seaxpay_log('webhook', sprintf('Risk refund initiated: estimated=%s, payer=%s, reason=%s', $estimated_refund, $payer_address, $risk_reason),
                $wc_order_id, $platform_order_no, seaxpay_sanitize_for_log($data), null);

        } elseif ($event === 'payment.risk_refunded' || $status === 'risk_refunded') {
            $risk_reason = $data['risk_reason'] ?? 'Unknown';
            $payer_address = $data['payer_address'] ?? 'N/A';
            $refund_tx_hash = $data['refund_tx_hash'] ?? 'N/A';
            $refund_amount = $data['refund_amount'] ?? '0';
            
            seaxpay_update_order_status($platform_order_no, 'risk_refunded');
            
            $wc_order = wc_get_order($wc_order_id);
            if ($wc_order) {
                $wc_order->update_status('refunded', sprintf(
                    /* translators: %1$s: refund amount, %2$s: payer wallet address, %3$s: risk reason */
                    __('Payment refunded to payer by risk control. Amount: %1$s. Payer address: %2$s. Reason: %3$s', 'paystarsea-crypto-payment-gateway'),
                    $refund_amount, $payer_address, $risk_reason
                ));
                
                $wc_order->add_order_note(sprintf(
                    /* translators: %1$s: refund transaction hash, %2$s: original payment hash */
                    __('Risk review completed: REFUNDED to payer. Funds have been returned to the original sender on-chain. Refund transaction hash: %1$s. Original payment hash: %2$s', 'paystarsea-crypto-payment-gateway'),
                    $refund_tx_hash, $data['tx_hash'] ?? 'N/A'
                ));
            }
            
            seaxpay_log('webhook', sprintf('Order risk refunded: amount=%s, payer=%s, refundTx=%s', $refund_amount, $payer_address, $refund_tx_hash), 
                $wc_order_id, $platform_order_no, seaxpay_sanitize_for_log($data), null);

        } elseif ($event === 'payment.risk_frozen' || $status === 'risk_frozen') {
            $risk_reason = $data['risk_reason'] ?? 'Sanctioned address detected';
            $payer_address = $data['payer_address'] ?? 'N/A';
            $tx_hash = $data['tx_hash'] ?? 'N/A';
            $amount_paid = $data['amount_paid'] ?? $data['crypto_amount'] ?? '0';

            seaxpay_update_order_status($platform_order_no, 'risk_frozen');

            $wc_order = wc_get_order($wc_order_id);
            if ($wc_order) {
                $wc_order->update_status('on-hold', sprintf(
                    /* translators: %1$s: payer wallet address, %2$s: risk reason */
                    __('Payment FROZEN by compliance control. Funds cannot be released or refunded due to sanctions. Payer address: %1$s. Reason: %2$s', 'paystarsea-crypto-payment-gateway'),
                    $payer_address, $risk_reason
                ));

                $wc_order->add_order_note(sprintf(
                    /* translators: %1$s: payment transaction hash, %2$s: frozen amount */
                    __('Risk review completed: FROZEN. The incoming funds came from a sanctioned or high-risk address and have been permanently locked on the gateway side. Payment transaction hash: %1$s. Amount: %2$s. Please contact gateway support with the transaction hash for compliance escalation.', 'paystarsea-crypto-payment-gateway'),
                    $tx_hash, $amount_paid
                ));
            }

            seaxpay_log('webhook', sprintf('Order risk frozen: payer=%s, txHash=%s, reason=%s', $payer_address, $tx_hash, $risk_reason),
                $wc_order_id, $platform_order_no, seaxpay_sanitize_for_log($data), null);

        } elseif ($event === 'merchant.withdraw_submitted') {
            $withdraw_id = $data['withdraw_id'] ?? 'N/A';
            $withdraw_tx_hash = $data['tx_hash'] ?? 'N/A';

            $wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
                "{$wpdb->prefix}seaxpay_orders",
                array(
                    'status' => 'withdrawing',
                    'platform_callback_data' => wp_json_encode($data)
                ),
                array('platform_order_no' => $platform_order_no)
            );

            $wc_order = wc_get_order($wc_order_id);
            if ($wc_order) {
                $wc_order->add_order_note(sprintf(
                    /* translators: %1$s: withdraw batch ID, %2$s: blockchain transaction hash */
                    __('Merchant withdrawal submitted on-chain. Withdraw batch: %1$s. Transaction hash: %2$s. Awaiting confirmations.', 'paystarsea-crypto-payment-gateway'),
                    $withdraw_id, $withdraw_tx_hash
                ));
            }

            seaxpay_log('webhook', sprintf('Withdraw submitted: withdrawId=%s, tx=%s', $withdraw_id, $withdraw_tx_hash),
                $wc_order_id, $platform_order_no, seaxpay_sanitize_for_log($data), null);

        } elseif ($event === 'merchant.withdraw_completed') {
            $withdraw_id = $data['withdraw_id'] ?? 'N/A';
            $withdraw_tx_hash = $data['tx_hash'] ?? 'N/A';

            $wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
                "{$wpdb->prefix}seaxpay_orders",
                array(
                    'status' => 'completed',
                    'platform_callback_data' => wp_json_encode($data)
                ),
                array('platform_order_no' => $platform_order_no)
            );

            $wc_order = wc_get_order($wc_order_id);
            if ($wc_order) {
                $wc_order->add_order_note(sprintf(
                    /* translators: %1$s: withdraw batch ID, %2$s: blockchain transaction hash */
                    __('Merchant withdrawal confirmed on-chain. Withdraw batch: %1$s. Transaction hash: %2$s. Settlement finalized.', 'paystarsea-crypto-payment-gateway'),
                    $withdraw_id, $withdraw_tx_hash
                ));
                if ($config->auto_complete_order == 1 && $wc_order->get_status() !== 'completed') {
                    $wc_order->update_status('completed', __('Order completed after merchant withdrawal confirmed on-chain.', 'paystarsea-crypto-payment-gateway'));
                }
            }

            seaxpay_log('webhook', sprintf('Withdraw completed: withdrawId=%s, tx=%s', $withdraw_id, $withdraw_tx_hash),
                $wc_order_id, $platform_order_no, seaxpay_sanitize_for_log($data), null);

        } elseif ($event === 'merchant.withdraw_failed') {
            $withdraw_id = $data['withdraw_id'] ?? 'N/A';
            $withdraw_tx_hash = $data['tx_hash'] ?? 'N/A';

            $wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
                "{$wpdb->prefix}seaxpay_orders",
                array(
                    'status' => 'pending_withdraw',
                    'platform_callback_data' => wp_json_encode($data)
                ),
                array('platform_order_no' => $platform_order_no)
            );

            $wc_order = wc_get_order($wc_order_id);
            if ($wc_order) {
                $wc_order->update_status('on-hold', sprintf(
                    /* translators: %1$s: withdraw batch ID, %2$s: last transaction hash */
                    __('Merchant withdrawal failed. Withdraw batch: %1$s. Last transaction hash: %2$s. Please retry withdrawal from the gateway dashboard.', 'paystarsea-crypto-payment-gateway'),
                    $withdraw_id, $withdraw_tx_hash
                ));
            }

            seaxpay_log('webhook', sprintf('Withdraw failed: withdrawId=%s, tx=%s', $withdraw_id, $withdraw_tx_hash),
                $wc_order_id, $platform_order_no, seaxpay_sanitize_for_log($data), null);

        } elseif ($event === 'payment.pending_withdraw' || $status === 'pending_withdraw') {
            $actual_amount_str = isset($data['actual_amount']) ? strval($data['actual_amount']) : '0';
            $pending_since = $data['pending_withdraw_time'] ?? gmdate('Y-m-d H:i:s');

            $wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
                "{$wpdb->prefix}seaxpay_orders",
                array(
                    'status' => 'pending_withdraw',
                    'customer_address' => $data['customer_address'] ?? null,
                    'tx_hash' => $data['tx_hash'] ?? null,
                    'paid_time' => isset($data['paid_time']) ? gmdate('Y-m-d H:i:s', strtotime($data['paid_time'])) : gmdate('Y-m-d H:i:s'),
                    'platform_callback_data' => wp_json_encode($data)
                ),
                array('platform_order_no' => $platform_order_no)
            );

            $wc_order = wc_get_order($wc_order_id);
            if ($wc_order) {
                $wc_order->add_order_note(sprintf(
                    /* translators: %1$s: confirmed crypto amount, %2$s: blockchain transaction hash */
                    __('Payment confirmed on-chain. Amount: %1$s. Transaction hash: %2$s. Funds are held at a derived address and will be transferred to the merchant wallet after withdrawal is initiated from the gateway dashboard.', 'paystarsea-crypto-payment-gateway'),
                    $actual_amount_str,
                    $data['tx_hash'] ?? 'N/A'
                ));
                if ($wc_order->get_status() !== 'on-hold' && $wc_order->get_status() !== 'completed') {
                    $wc_order->update_status('on-hold', sprintf(
                        /* translators: %s: date/time since when withdrawal is pending */
                        __('Payment confirmed, awaiting merchant withdrawal since %s.', 'paystarsea-crypto-payment-gateway'),
                        $pending_since
                    ));
                }
            }

            seaxpay_log('webhook', sprintf('Order pending_withdraw: amount=%s, tx=%s', $actual_amount_str, $data['tx_hash'] ?? 'N/A'),
                $wc_order_id, $platform_order_no, seaxpay_sanitize_for_log($data), null);

        } elseif ($event === 'payment.pending_withdraw_expire_warn') {
            $pending_since = $data['pending_withdraw_time'] ?? 'unknown';

            $wc_order = wc_get_order($wc_order_id);
            if ($wc_order) {
                $wc_order->add_order_note(sprintf(
                    /* translators: %s: date/time since when withdrawal is pending */
                    __('Pending withdraw expiry warning: Order has been awaiting merchant withdrawal since %s. Please initiate withdrawal from the gateway dashboard soon to avoid auto-expiry.', 'paystarsea-crypto-payment-gateway'),
                    $pending_since
                ));
            }

            seaxpay_log('webhook', sprintf('Pending withdraw expire warning: pendingSince=%s', $pending_since),
                $wc_order_id, $platform_order_no, seaxpay_sanitize_for_log($data), null);

        } elseif ($event === 'merchant.withdraw_review_timeout') {
            $withdraw_id = $data['withdraw_id'] ?? 'N/A';
            $timeout_hours = $data['timeout_hours'] ?? 'N/A';
            $pending_since = $data['pending_withdraw_time'] ?? 'unknown';

            $wc_order = wc_get_order($wc_order_id);
            if ($wc_order) {
                $wc_order->add_order_note(sprintf(
                    /* translators: %1$s: withdraw batch ID, %2$s: timeout hours, %3$s: pending since date */
                    __('Withdraw review timeout alert: Withdraw batch #%1$s has been awaiting gateway operator review for over %2$s hours (pending since %3$s). The batch will remain queued until approved; please contact gateway support if the delay persists.', 'paystarsea-crypto-payment-gateway'),
                    $withdraw_id, $timeout_hours, $pending_since
                ));
            }

            seaxpay_log('webhook', sprintf('Withdraw review timeout alert: withdrawId=%s, timeoutHours=%s, pendingSince=%s',
                $withdraw_id, $timeout_hours, $pending_since),
                $wc_order_id, $platform_order_no, seaxpay_sanitize_for_log($data), null);

        } elseif ($event === 'merchant.withdraw_review_approved') {
            $withdraw_id = $data['withdraw_id'] ?? 'N/A';

            $wc_order = wc_get_order($wc_order_id);
            if ($wc_order) {
                $wc_order->add_order_note(sprintf(
                    /* translators: %s: withdraw batch ID */
                    __('Withdraw batch #%s approved by gateway operator. Awaiting on-chain execution.', 'paystarsea-crypto-payment-gateway'),
                    $withdraw_id
                ));
            }

            seaxpay_log('webhook', sprintf('Withdraw review approved: withdrawId=%s', $withdraw_id),
                $wc_order_id, $platform_order_no, seaxpay_sanitize_for_log($data), null);

        } elseif ($event === 'merchant.withdraw_review_rejected') {
            $withdraw_id = $data['withdraw_id'] ?? 'N/A';

            $wc_order = wc_get_order($wc_order_id);
            if ($wc_order) {
                $wc_order->add_order_note(sprintf(
                    /* translators: %s: withdraw batch ID */
                    __('Withdraw batch #%s rejected by gateway operator. Order returned to pending withdrawal; you may submit a new withdraw request.', 'paystarsea-crypto-payment-gateway'),
                    $withdraw_id
                ));
            }

            seaxpay_log('webhook', sprintf('Withdraw review rejected: withdrawId=%s', $withdraw_id),
                $wc_order_id, $platform_order_no, seaxpay_sanitize_for_log($data), null);

        } elseif ($event === 'merchant.withdraw_review_frozen') {
            $withdraw_id = $data['withdraw_id'] ?? 'N/A';

            $wc_order = wc_get_order($wc_order_id);
            if ($wc_order) {
                $wc_order->add_order_note(sprintf(
                    /* translators: %s: withdraw batch ID */
                    __('Withdraw batch #%s frozen by gateway operator (compliance hold). Please contact gateway support for further details.', 'paystarsea-crypto-payment-gateway'),
                    $withdraw_id
                ));
            }

            seaxpay_log('webhook', sprintf('Withdraw review frozen: withdrawId=%s', $withdraw_id),
                $wc_order_id, $platform_order_no, seaxpay_sanitize_for_log($data), null);

        } elseif ($event === 'payment.overpaid' || $status === 'overpaid') {
            $risk_reason = $data['risk_reason'] ?? 'Overpayment detected';
            $payer_address = $data['payer_address'] ?? 'N/A';
            $tx_hash = $data['tx_hash'] ?? 'N/A';
            $actual_amount = $data['actual_amount'] ?? '0';
            $crypto_amount = $data['crypto_amount'] ?? '0';

            $wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
                "{$wpdb->prefix}seaxpay_orders",
                array(
                    'status' => 'overpaid',
                    'platform_callback_data' => wp_json_encode($data)
                ),
                array('platform_order_no' => $platform_order_no)
            );

            $wc_order = wc_get_order($wc_order_id);
            if ($wc_order) {
                $wc_order->add_order_note(sprintf(
                    /* translators: %1$s: expected amount, %2$s: received amount, %3$s: payer address, %4$s: reason, %5$s: transaction hash */
                    __('Payment detected as overpaid (expected %1$s, received %2$s). Gateway will refund the full amount to payer %3$s. Reason: %4$s. Tx: %5$s', 'paystarsea-crypto-payment-gateway'),
                    $crypto_amount, $actual_amount, $payer_address, $risk_reason, $tx_hash
                ));
            }

            seaxpay_log('webhook', sprintf('Order overpaid: expected=%s, received=%s, payer=%s, tx=%s, reason=%s',
                $crypto_amount, $actual_amount, $payer_address, $tx_hash, $risk_reason),
                $wc_order_id, $platform_order_no, seaxpay_sanitize_for_log($data), null);

        } elseif ($event === 'payment.auto_expire_withdraw_triggered') {
            $withdraw_id = $data['withdraw_id'] ?? 'N/A';
            $trigger_source = $data['trigger_source'] ?? 'auto_expire';
            $pending_since = $data['pending_withdraw_time'] ?? 'unknown';

            $wc_order = wc_get_order($wc_order_id);
            if ($wc_order) {
                $wc_order->add_order_note(sprintf(
                    /* translators: %1$s: withdraw batch ID, %2$s: pending since date, %3$s: trigger source */
                    __('Auto-triggered withdraw batch #%1$s created by gateway (pending since %2$s, trigger=%3$s). Funds will be collected to your default wallet shortly.', 'paystarsea-crypto-payment-gateway'),
                    $withdraw_id, $pending_since, $trigger_source
                ));
            }

            seaxpay_log('webhook', sprintf('Auto-expire withdraw triggered: withdrawId=%s, pendingSince=%s',
                $withdraw_id, $pending_since),
                $wc_order_id, $platform_order_no, seaxpay_sanitize_for_log($data), null);

        } elseif ($event === 'payment.pending_withdraw_no_wallet') {
            $pending_since = $data['pending_withdraw_time'] ?? 'unknown';

            $wc_order = wc_get_order($wc_order_id);
            if ($wc_order) {
                $wc_order->add_order_note(sprintf(
                    /* translators: %s: date/time since when withdrawal is pending */
                    __('ACTION REQUIRED: Gateway attempted auto-collection for this pending-withdraw order (pending since %s) but found no active wallet configured for this coin/network. Please configure a wallet in the merchant dashboard immediately, or contact gateway support.', 'paystarsea-crypto-payment-gateway'),
                    $pending_since
                ));
            }

            seaxpay_log('webhook', sprintf('Auto-expire no-active-wallet alert: pendingSince=%s', $pending_since),
                $wc_order_id, $platform_order_no, seaxpay_sanitize_for_log($data), null);
        } else {
            seaxpay_log('error', sprintf('Unknown webhook event, plugin may be out of sync with gateway: event=%s, status=%s', $event, $status),
                $wc_order_id, $platform_order_no, seaxpay_sanitize_for_log($data), null);
        }
    }
    
    private function sanitize_webhook_payload($data) {
        if (!is_array($data)) {
            return array();
        }

        $sanitized = array();
        foreach ($data as $key => $value) {
            $safe_key = is_string($key) ? sanitize_text_field($key) : $key;
            if ($safe_key === '' || $safe_key === null) {
                continue;
            }
            if (is_array($value)) {
                $sanitized[$safe_key] = $this->sanitize_webhook_payload($value);
            } elseif (is_numeric($value)) {
                $sanitized[$safe_key] = $value;
            } else {
                $sanitized[$safe_key] = sanitize_text_field((string) $value);
            }
        }

        return $sanitized;
    }

    private function seq_compare($a, $b) {
        if (function_exists('bccomp')) {
            return bccomp($a, $b, 0);
        }
        $a = ltrim($a, '0');
        $b = ltrim($b, '0');
        if ($a === '') { $a = '0'; }
        if ($b === '') { $b = '0'; }
        $la = strlen($a);
        $lb = strlen($b);
        if ($la !== $lb) {
            return $la < $lb ? -1 : 1;
        }
        return strcmp($a, $b);
    }

    private function send_success_response($message = 'Success') {
        wp_send_json(array(
            'code' => 0,
            'msg' => $message
        ), 200);
    }
    
    private function send_error_response($code, $message) {
        $http_status = 400;
        if ($code == 401) {
            $http_status = 401;
        } elseif ($code == 404) {
            $http_status = 404;
        } elseif ($code >= 500) {
            $http_status = 500;
        }
        wp_send_json(array(
            'code' => $code,
            'msg' => $message
        ), $http_status);
    }
}

new SeaXPay_Webhook();
