<?php
/**
 * PayPal Handler for BoostBot.
 *
 * @package BoostBot
 */

// Prevent direct access to the file.
if (!defined('ABSPATH')) {
    exit;
}

/**
 * BoostBot_PayPal_Handler class.
 *
 * Handles PayPal IPN (Instant Payment Notification) for subscription lifecycle.
 *
 * Payment Flow:
 * 1. User subscribes via PayPal button on the frontend (UsageDashboard).
 * 2. PayPal sends IPN POST to /wp-json/boostbot/v1/paypal/ipn.
 * 3. This handler verifies the IPN with PayPal's servers (verify_ipn).
 * 4. Duplicate transaction IDs are rejected (is_duplicate_transaction).
 * 5. Based on txn_type, the handler activates/deactivates the Pro license.
 * 6. Events are logged to the boostbot_paypal_ipn_log option.
 *
 * Note: As of Week 26+, PayPal billing is primarily backend-owned.
 * This handler serves as a legacy/fallback IPN listener.
 *
 * @since 1.0.0
 */
class BoostBot_PayPal_Handler
{
    /**
     * PayPal IPN verification URL (production).
     *
     * @since 1.0.0
     * @access private
     * @var string
     */
    private $ipn_verify_url = 'https://ipnpb.paypal.com/cgi-bin/webscr';

    /**
     * PayPal IPN verification URL (sandbox).
     *
     * @since 1.0.0
     * @access private
     * @var string
     */
    private $ipn_verify_url_sandbox = 'https://ipnpb.sandbox.paypal.com/cgi-bin/webscr';

    /**
     * Whether to use sandbox mode.
     *
     * @since 1.0.0
     * @access private
     * @var bool
     */
    private $sandbox_mode = false;

    /**
     * License manager instance.
     *
     * @since 1.0.0
     * @access private
     * @var BoostBot_License_Manager
     */
    private $license_manager;

    /**
     * Constructor.
     *
     * @since 1.0.0
     */
    public function __construct()
    {
        $this->license_manager = new BoostBot_License_Manager();
        $this->sandbox_mode = defined('BOOSTBOT_PAYPAL_SANDBOX') && BOOSTBOT_PAYPAL_SANDBOX;
    }

    /**
     * Initialize PayPal handler hooks.
     *
     * @since 1.0.0
     */
    public function init()
    {
        // Register IPN endpoint
        add_action('rest_api_init', [$this, 'register_ipn_endpoint']);
    }

    /**
     * Register the IPN endpoint.
     *
     * @since 1.0.0
     */
    public function register_ipn_endpoint()
    {
        register_rest_route('boostbot/v1', '/paypal/ipn', [
            'methods'             => 'POST',
            'callback'            => [$this, 'handle_ipn'],
            'permission_callback' => '__return_true', // PayPal needs access
        ]);
    }

    /**
     * Handle incoming IPN notification.
     *
     * @since 1.0.0
     * @param WP_REST_Request $request The request object.
     * @return WP_REST_Response Response.
     */
    public function handle_ipn($request)
    {
        // Get raw POST data
        $raw_post_data = $request->get_body();
        parse_str($raw_post_data, $data);

        BoostBot_Logger::debug('PayPal IPN received', [
            'txn_type' => $data['txn_type'] ?? 'unknown',
        ]);

        // Verify IPN is from PayPal
        if (!$this->verify_ipn($raw_post_data)) {
            BoostBot_Logger::error('PayPal IPN verification failed');
            return new WP_REST_Response(['error' => 'Invalid IPN'], 400);
        }

        // Replay protection: Reject duplicate transaction IDs.
        $txn_id = isset( $data['txn_id'] ) ? sanitize_text_field( $data['txn_id'] ) : '';
        if ( ! empty( $txn_id ) && $this->is_duplicate_transaction( $txn_id ) ) {
            BoostBot_Logger::warning( 'PayPal IPN duplicate txn_id rejected', array( 'txn_id' => $txn_id ) );
            return new WP_REST_Response( array( 'success' => true, 'note' => 'duplicate' ), 200 );
        }

        // Process based on transaction type
        $txn_type = $data['txn_type'] ?? '';
        $payment_status = $data['payment_status'] ?? '';

        switch ($txn_type) {
            case 'subscr_signup':
                // New subscription created. subscr_signup carries no txn_id, so
                // dedupe by subscription id to guard against replayed signups.
                $subscr_id = isset( $data['subscr_id'] ) ? sanitize_text_field( $data['subscr_id'] ) : '';
                if ( ! empty( $subscr_id ) && $this->is_duplicate_transaction( $subscr_id, 'boostbot_ipn_subscr_' ) ) {
                    BoostBot_Logger::warning( 'PayPal IPN duplicate subscr_signup rejected', array( 'subscr_id' => $subscr_id ) );
                    break;
                }
                $this->handle_subscription_signup($data);
                break;

            case 'subscr_payment':
                // Subscription payment received
                $this->handle_subscription_payment($data);
                break;

            case 'subscr_cancel':
                // Subscription cancelled
                $this->handle_subscription_cancel($data);
                break;

            case 'subscr_eot':
                // Subscription expired (end of term)
                $this->handle_subscription_expired($data);
                break;

            case 'subscr_failed':
                // Payment failed
                $this->handle_payment_failed($data);
                break;

            case 'web_accept':
            case 'cart':
                // One-time payment
                if ($payment_status === 'Completed') {
                    $this->handle_one_time_payment($data);
                }
                break;

            case 'refunded':
            case 'reversed':
                // Refund or chargeback
                $this->handle_refund($data);
                break;
        }

        // Always return 200 to PayPal
        return new WP_REST_Response(['success' => true], 200);
    }

    /**
     * Verify IPN is from PayPal.
     *
     * @since 1.0.0
     * @param string $raw_data Raw POST data.
     * @return bool True if valid.
     */
    private function verify_ipn($raw_data)
    {
        // Add verify command
        $verify_data = 'cmd=_notify-validate&' . $raw_data;

        // Determine verify URL
        $verify_url = $this->sandbox_mode
            ? $this->ipn_verify_url_sandbox
            : $this->ipn_verify_url;

        // Send to PayPal for verification
        $response = wp_remote_post($verify_url, [
            'body'        => $verify_data,
            'timeout'     => 60,
            'sslverify'   => true,
            'httpversion' => '1.1',
            'headers'     => [
                'Connection' => 'Close',
            ],
        ]);

        if (is_wp_error($response)) {
            BoostBot_Logger::error('PayPal IPN verify request failed', [
                'error' => $response->get_error_message(),
            ]);
            return false;
        }

        $body = wp_remote_retrieve_body($response);

        return $body === 'VERIFIED';
    }

    /**
     * Handle new subscription signup.
     *
     * @since 1.0.0
     * @param array $data IPN data.
     */
    private function handle_subscription_signup($data)
    {
        $subscription_id = sanitize_text_field($data['subscr_id'] ?? '');
        $payer_email = sanitize_email($data['payer_email'] ?? '');

        if (empty($subscription_id) || empty($payer_email)) {
            BoostBot_Logger::error('Invalid subscription signup data');
            return;
        }

        // Activate Pro license
        $this->license_manager->activate_pro($subscription_id, $payer_email);

        // Save additional subscription info
        update_option('boostbot_subscription_active', true);

        $this->log_ipn_event('subscription_signup', $data);

        BoostBot_Logger::info('New subscription created', [
            'subscription_id' => $subscription_id,
            'payer_email'     => $payer_email,
        ]);
    }

    /**
     * Handle subscription payment.
     *
     * @since 1.0.0
     * @param array $data IPN data.
     */
    private function handle_subscription_payment($data)
    {
        $payment_status = $data['payment_status'] ?? '';

        if ($payment_status !== 'Completed') {
            BoostBot_Logger::warning('Subscription payment not completed', [
                'status' => $payment_status,
            ]);
            return;
        }

        $subscription_id = sanitize_text_field($data['subscr_id'] ?? '');
        $amount = floatval($data['mc_gross'] ?? 0);
        $currency = sanitize_text_field($data['mc_currency'] ?? 'USD');
        $transaction_id = sanitize_text_field($data['txn_id'] ?? '');

        // Ensure Pro is active
        $current_status = $this->license_manager->get_license_status();
        if ($current_status !== BoostBot_License_Manager::LICENSE_PRO) {
            $payer_email = sanitize_email($data['payer_email'] ?? '');
            $this->license_manager->activate_pro($subscription_id, $payer_email);
        }

        // Record payment
        $this->license_manager->record_payment([
            'amount'          => $amount,
            'currency'        => $currency,
            'transaction_id'  => $transaction_id,
            'subscription_id' => $subscription_id,
        ]);

        $this->log_ipn_event('subscription_payment', $data);

        BoostBot_Logger::info('Subscription payment received', [
            'amount'         => $amount,
            'currency'       => $currency,
            'transaction_id' => $transaction_id,
        ]);
    }

    /**
     * Handle subscription cancellation.
     *
     * @since 1.0.0
     * @param array $data IPN data.
     */
    private function handle_subscription_cancel($data)
    {
        $subscription_id = sanitize_text_field($data['subscr_id'] ?? '');

        BoostBot_Logger::warning('Subscription cancelled', [
            'subscription_id' => $subscription_id,
        ]);

        // Don't immediately deactivate - let them use until end of period
        update_option('boostbot_subscription_cancelled', true);
        update_option('boostbot_subscription_cancel_date', current_time('mysql'));

        $this->log_ipn_event('subscription_cancel', $data);

        // Send cancellation notice
        $this->send_cancellation_notice();
    }

    /**
     * Handle subscription expiration.
     *
     * @since 1.0.0
     * @param array $data IPN data.
     */
    private function handle_subscription_expired($data)
    {
        $subscription_id = sanitize_text_field($data['subscr_id'] ?? '');

        BoostBot_Logger::warning('Subscription expired', [
            'subscription_id' => $subscription_id,
        ]);

        // Deactivate subscription
        $this->license_manager->deactivate_subscription();
        update_option('boostbot_subscription_active', false);

        $this->log_ipn_event('subscription_expired', $data);
    }

    /**
     * Handle failed payment.
     *
     * @since 1.0.0
     * @param array $data IPN data.
     */
    private function handle_payment_failed($data)
    {
        BoostBot_Logger::warning('Subscription payment failed', $data);

        // Send notification to admin
        $this->send_payment_failed_notice();

        $this->log_ipn_event('payment_failed', $data);
    }

    /**
     * Handle one-time payment (manual activation needed).
     *
     * @since 1.0.0
     * @param array $data IPN data.
     */
    private function handle_one_time_payment($data)
    {
        $amount = floatval($data['mc_gross'] ?? 0);
        $payer_email = sanitize_email($data['payer_email'] ?? '');
        $transaction_id = sanitize_text_field($data['txn_id'] ?? '');

        BoostBot_Logger::info('One-time payment received', [
            'amount'         => $amount,
            'payer_email'    => $payer_email,
            'transaction_id' => $transaction_id,
        ]);

        // For one-time payments, admin needs to manually activate
        // Or auto-activate if amount matches monthly price
        if ($amount >= BoostBot_License_Manager::$monthly_price) {
            $this->license_manager->activate_pro($transaction_id, $payer_email);
            $this->license_manager->record_payment([
                'amount'         => $amount,
                'transaction_id' => $transaction_id,
            ]);
        }

        $this->log_ipn_event('one_time_payment', $data);
    }

    /**
     * Handle refund or chargeback.
     *
     * @since 1.0.0
     * @param array $data IPN data.
     */
    private function handle_refund($data)
    {
        BoostBot_Logger::warning('Refund or chargeback received', $data);

        // Deactivate subscription
        $this->license_manager->deactivate_subscription();

        $this->log_ipn_event('refund', $data);
    }

    /**
     * Check if a transaction ID has already been processed (replay protection).
     *
     * @since 1.0.0
     * @param string $txn_id        Transaction (or subscription) ID.
     * @param string $option_prefix Option-name prefix for the per-id marker.
     * @return bool True if duplicate.
     */
    private function is_duplicate_transaction( $txn_id, $option_prefix = 'boostbot_ipn_txn_' ) {
        if ( empty( $txn_id ) ) {
            return false;
        }

        // Atomic first-write-wins marker. add_option() returns false when the
        // option already exists, so a concurrent/replayed IPN can never win the
        // race the way the old get/in_array/update sequence could. autoload 'no'
        // keeps these markers out of the alloptions cache. md5() normalizes the
        // id to a fixed-length, safe option-name suffix.
        $marker = $option_prefix . md5( (string) $txn_id );
        if ( false === add_option( $marker, 1, '', 'no' ) ) {
            return true;
        }

        // Legacy fallback: honor the pre-existing bounded array of processed
        // txn_ids recorded before per-transaction markers existed (read-only).
        $processed = get_option( 'boostbot_paypal_processed_txns', array() );
        if ( is_array( $processed ) && in_array( $txn_id, $processed, true ) ) {
            return true;
        }

        return false;
    }

    /**
     * Log IPN event to database.
     *
     * @since 1.0.0
     * @param string $event_type Event type.
     * @param array  $data       IPN data.
     */
    private function log_ipn_event($event_type, $data)
    {
        $log = get_option('boostbot_paypal_ipn_log', []);

        $log[] = [
            'type'      => $event_type,
            'timestamp' => current_time('mysql'),
            'txn_id'    => $data['txn_id'] ?? '',
            'subscr_id' => $data['subscr_id'] ?? '',
            'amount'    => $data['mc_gross'] ?? '',
            'status'    => $data['payment_status'] ?? '',
        ];

        // Keep last 100 events
        if (count($log) > 100) {
            array_shift($log);
        }

        update_option('boostbot_paypal_ipn_log', $log);
    }

    /**
     * Send cancellation notice to admin.
     *
     * @since 1.0.0
     */
    private function send_cancellation_notice()
    {
        $admin_email = get_option('admin_email');
        $site_name = get_bloginfo('name');

        $subject = sprintf(
            /* translators: %s: site name */
            __('[%s] מנוי BoostBot Pro בוטל', 'boostbot'),
            $site_name
        );

        $message = __(
            'שלום,

מנוי BoostBot Pro שלך בוטל.

המנוי ימשיך לפעול עד סוף התקופה ששולמה.

אם ברצונך לחדש, תוכל להירשם מחדש דרך הגדרות התוסף.

צוות BoostBot',
            'boostbot'
        );

        wp_mail($admin_email, $subject, $message);
    }

    /**
     * Send payment failed notice.
     *
     * @since 1.0.0
     */
    private function send_payment_failed_notice()
    {
        $admin_email = get_option('admin_email');
        $site_name = get_bloginfo('name');

        $subject = sprintf(
            /* translators: %s: site name */
            __('[%s] תשלום BoostBot נכשל', 'boostbot'),
            $site_name
        );

        $message = sprintf(
            /* translators: %s: upgrade URL */
            __(
                'שלום,

התשלום האחרון עבור מנוי BoostBot Pro נכשל.

אנא עדכן את פרטי התשלום שלך או בצע תשלום ידני:
%s

אם לא נקבל תשלום, המנוי יבוטל.

צוות BoostBot',
                'boostbot'
            ),
            $this->license_manager->get_upgrade_url()
        );

        wp_mail($admin_email, $subject, $message);
    }

    /**
     * Get IPN log.
     *
     * @since 1.0.0
     * @param int $limit Number of records.
     * @return array IPN log.
     */
    public function get_ipn_log($limit = 50)
    {
        $log = get_option('boostbot_paypal_ipn_log', []);
        return array_slice(array_reverse($log), 0, $limit);
    }
}










