<?php
/**
 * Stripe OAuth 2.0 Connection Handler
 *
 * Implements the "Connect with Stripe" flow using Stripe's standard OAuth 2.0
 * protocol (Stripe Connect). This replaces manual API-key copy-paste with a
 * secure 1-click authorisation flow, satisfying Stripe's platform authentication
 * requirements effective 2025.
 *
 * SETUP:
 * 1. Register your WordPress site as a Stripe App at:
 *    https://dashboard.stripe.com/settings/connect
 * 2. Copy the Client ID and Client Secret into the plugin settings page.
 * 3. Whitelist the redirect URI shown in the settings page.
 * 4. Click "Connect with Stripe" â€” done.
 *
 * Once connected the access_token is used automatically by STRP_SUB_Stripe_API
 * instead of the manually-entered sk_live / sk_test keys.
 */

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

class STRP_SUB_OAuth {

    private static $instance = null;

    // â”€â”€ WordPress option keys â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
    const OPTION_TEST_ACCESS_TOKEN  = 'strp_sub_oauth_test_access_token';
    const OPTION_LIVE_ACCESS_TOKEN  = 'strp_sub_oauth_live_access_token';
    const OPTION_STRIPE_USER_ID     = 'strp_sub_oauth_stripe_user_id';
    const OPTION_PUBLISHABLE_KEY    = 'strp_sub_oauth_publishable_key';
    const OPTION_CLIENT_ID          = 'strp_sub_oauth_client_id';
    const OPTION_CLIENT_SECRET      = 'strp_sub_oauth_client_secret';
    const OPTION_LIVEMODE           = 'strp_sub_oauth_livemode';

    // â”€â”€ Stripe OAuth endpoints â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
    const AUTHORIZE_URL   = 'https://connect.stripe.com/oauth/authorize';
    const TOKEN_URL       = 'https://connect.stripe.com/oauth/token';
    const DEAUTHORIZE_URL = 'https://connect.stripe.com/oauth/deauthorize';

    private static function encrypt_value( $plaintext ) {
        if ( empty( $plaintext ) || ! function_exists( 'openssl_encrypt' ) ) {
            return $plaintext;
        }
        $key = hash( 'sha256', LOGGED_IN_KEY . LOGGED_IN_SALT, true );
        $iv  = openssl_random_pseudo_bytes( 16 );
        $tag = '';
        $encrypted = openssl_encrypt( $plaintext, 'aes-256-gcm', $key, OPENSSL_RAW_DATA, $iv, $tag, '', 16 );
        if ( false === $encrypted ) {
            return $plaintext;
        }
        return 'enc:' . base64_encode( $iv . $tag . $encrypted );
    }

    private static function decrypt_value( $ciphertext ) {
        if ( empty( $ciphertext ) || ! function_exists( 'openssl_decrypt' ) ) {
            return $ciphertext;
        }
        if ( 0 !== strpos( $ciphertext, 'enc:' ) ) {
            return $ciphertext; // Legacy plain-text value — will be re-encrypted on next save.
        }
        $data = base64_decode( substr( $ciphertext, 4 ) );
        if ( strlen( $data ) < 32 ) {
            return '';
        }
        $key  = hash( 'sha256', LOGGED_IN_KEY . LOGGED_IN_SALT, true );
        $iv   = substr( $data, 0, 16 );
        $tag  = substr( $data, 16, 16 );
        $raw  = substr( $data, 32 );
        $plain = openssl_decrypt( $raw, 'aes-256-gcm', $key, OPENSSL_RAW_DATA, $iv, $tag );
        return ( false === $plain ) ? '' : $plain;
    }

    public static function get_instance() {
        if ( null === self::$instance ) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    private function __construct() {
        add_action( 'rest_api_init',         array( $this, 'register_callback_endpoint' ) );
        add_action( 'admin_post_strp_sub_oauth_connect',    array( $this, 'initiate_redirect' ) );
        add_action( 'admin_post_strp_sub_oauth_disconnect', array( $this, 'disconnect' ) );
    }

    private function __clone() {}

    public function __wakeup() {
        throw new \RuntimeException( 'Cannot unserialize singleton.' );
    }

    // â”€â”€ Public static API â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€

    /**
     * Whether the plugin currently has a valid OAuth connection.
     */
    public static function is_connected() {
        return '' !== self::get_access_token();
    }

    /**
     * Return the stored OAuth access token for the current mode (test / live).
     *
     * @return string Empty string when not connected.
     */
    public static function get_access_token() {
        $option = STRP_SUB_Settings::is_test_mode()
            ? self::OPTION_TEST_ACCESS_TOKEN
            : self::OPTION_LIVE_ACCESS_TOKEN;

        return self::decrypt_value( (string) get_option( $option, '' ) );
    }

    /**
     * Return the connected Stripe account's publishable key.
     */
    public static function get_publishable_key() {
        return self::decrypt_value( (string) get_option( self::OPTION_PUBLISHABLE_KEY, '' ) );
    }

    /**
     * Return the configured Stripe OAuth Client ID (ca_xxxx).
     */
    public static function get_client_id() {
        if ( defined( 'STRP_SUB_OAUTH_CLIENT_ID' ) ) {
            return STRP_SUB_OAUTH_CLIENT_ID;
        }
        return (string) get_option( self::OPTION_CLIENT_ID, '' );
    }

    /**
     * Return the configured Stripe OAuth Client Secret (decrypted from storage).
     */
    public static function get_client_secret() {
        return STRP_SUB_Settings::get_oauth_client_secret();
    }

    /**
     * The redirect URI Stripe will call after the user authorises the connection.
     * This URL must be whitelisted in the Stripe Dashboard â†’ Connect settings.
     */
    public static function get_redirect_uri() {
        return rest_url( 'stripe-sub/v1/oauth/callback' );
    }

    /**
     * Build the "Connect with Stripe" authorisation URL.
     *
     * @return string|false Full URL or false when no Client ID is set.
     */
    public static function get_connect_url() {
        $client_id = self::get_client_id();
        if ( empty( $client_id ) ) {
            return false;
        }

        // Generate and store a CSRF state token (10-minute TTL).
        $state = wp_generate_password( 32, false );
        set_transient( 'strp_sub_oauth_state_' . hash( 'sha256', $state ), get_current_user_id(), 600 );

        return add_query_arg(
            array(
                'response_type' => 'code',
                'client_id'     => $client_id,
                'scope'         => 'read_write',
                'redirect_uri'  => self::get_redirect_uri(),
                'state'         => $state,
            ),
            self::AUTHORIZE_URL
        );
    }

    // â”€â”€ Hooks â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€

    /**
     * Register the REST endpoint that Stripe redirects to after OAuth.
     */
    public function register_callback_endpoint() {
        register_rest_route(
            'stripe-sub/v1',
            '/oauth/callback',
            array(
                'methods'             => 'GET',
                'callback'            => array( $this, 'handle_callback' ),
                'permission_callback' => function() {
                    return current_user_can( 'manage_options' );
                },
            )
        );
    }

    /**
     * Redirect the admin to Stripe's authorisation page when they click
     * "Connect with Stripe".
     */
    public function initiate_redirect() {
        if ( ! current_user_can( 'manage_options' ) ) {
            wp_die( esc_html__( 'Permission denied.', 'subscription-manager-for-stripe' ) );
        }
        check_admin_referer( 'strp_sub_oauth_connect' );

        $url = self::get_connect_url();
        if ( ! $url ) {
            set_transient( 'strp_sub_oauth_error_' . get_current_user_id(), 'no_client_id', 60 );
            wp_safe_redirect( admin_url( 'admin.php?page=strp-sub-settings' ) );
            exit;
        }

        // Intentional external redirect to Stripe Connect. Cannot use wp_safe_redirect().
        wp_redirect( $url );
        exit;
    }

    /**
     * Handle the OAuth callback from Stripe.
     *
     * Stripe calls: GET /wp-json/stripe-sub/v1/oauth/callback?code=...&state=...
     */
    public function handle_callback( WP_REST_Request $request ) {
        $settings_url = admin_url( 'admin.php?page=strp-sub-settings' );

        // Ensure only administrators can complete the OAuth callback.
        if ( ! current_user_can( 'manage_options' ) ) {
            STRP_SUB_Logger::error( 'Stripe OAuth callback received from non-admin user.' );
            wp_safe_redirect( $settings_url );
            exit;
        }

        // â”€â”€ Error from Stripe â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
        $error = $request->get_param( 'error' );
        if ( $error ) {
            $desc = $request->get_param( 'error_description' ) ?? $error;
            STRP_SUB_Logger::error( 'Stripe OAuth error: ' . $error . ' â€” ' . $desc );
            set_transient( 'strp_sub_oauth_error_' . get_current_user_id(), $error . ': ' . $desc, 60 );
            wp_safe_redirect( $settings_url );
            exit;
        }

        // â”€â”€ CSRF state validation â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
        $state = (string) $request->get_param( 'state' );
        $expected_user = get_transient( 'strp_sub_oauth_state_' . hash( 'sha256', $state ) );
        if ( empty( $state ) || false === $expected_user || (int) $expected_user !== get_current_user_id() ) {
            STRP_SUB_Logger::error( 'Stripe OAuth: invalid state token â€” possible CSRF attack.' );
            set_transient( 'strp_sub_oauth_error_' . get_current_user_id(), 'invalid_state', 60 );
            wp_safe_redirect( $settings_url );
            exit;
        }
        delete_transient( 'strp_sub_oauth_state_' . hash( 'sha256', $state ) );

        // â”€â”€ Exchange code for token â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
        $code = (string) $request->get_param( 'code' );
        if ( empty( $code ) ) {
            wp_safe_redirect( add_query_arg( 'oauth_error', 'missing_code', $settings_url ) );
            exit;
        }

        $token_data = $this->exchange_code( $code );
        if ( is_wp_error( $token_data ) ) {
            STRP_SUB_Logger::error( 'Stripe OAuth token exchange failed: ' . $token_data->get_error_message() );
            set_transient( 'strp_sub_oauth_error_' . get_current_user_id(), $token_data->get_error_message(), 60 );
            wp_safe_redirect( $settings_url );
            exit;
        }

        // â”€â”€ Persist credentials â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
        $is_live = ! empty( $token_data['livemode'] ) && true === $token_data['livemode'];
        $option  = $is_live ? self::OPTION_LIVE_ACCESS_TOKEN : self::OPTION_TEST_ACCESS_TOKEN;

        update_option( $option,                   self::encrypt_value( sanitize_text_field( $token_data['access_token'] ) ) );
        update_option( self::OPTION_LIVEMODE,     $is_live ? 'yes' : 'no' );

        if ( ! empty( $token_data['stripe_user_id'] ) ) {
            update_option( self::OPTION_STRIPE_USER_ID, sanitize_text_field( $token_data['stripe_user_id'] ) );
        }
        if ( ! empty( $token_data['stripe_publishable_key'] ) ) {
            update_option( self::OPTION_PUBLISHABLE_KEY, self::encrypt_value( sanitize_text_field( $token_data['stripe_publishable_key'] ) ) );
        }

        STRP_SUB_Logger::log( 'Stripe OAuth: connected to account ' . ( $token_data['stripe_user_id'] ?? 'unknown' ) . ' (' . ( $is_live ? 'live' : 'test' ) . ' mode).' );

        set_transient( 'strp_sub_oauth_success_' . get_current_user_id(), true, 60 );
        wp_safe_redirect( $settings_url );
        exit;
    }

    /**
     * Disconnect the currently linked Stripe account (revoke on Stripe + delete local tokens).
     */
    public function disconnect() {
        if ( ! current_user_can( 'manage_options' ) ) {
            wp_die( esc_html__( 'Permission denied.', 'subscription-manager-for-stripe' ) );
        }
        check_admin_referer( 'strp_sub_oauth_disconnect' );

        $stripe_user_id = get_option( self::OPTION_STRIPE_USER_ID, '' );
        $client_id      = self::get_client_id();
        $client_secret  = self::get_client_secret();

        // Tell Stripe to revoke the connection so it appears in their dashboard.
        if ( $stripe_user_id && $client_id && $client_secret ) {
            wp_remote_post(
                self::DEAUTHORIZE_URL,
                array(
                    'headers' => array(
                        'Authorization' => 'Basic ' . base64_encode( $client_secret . ':' ),
                        'Content-Type'  => 'application/x-www-form-urlencoded',
                    ),
                    'body'    => array(
                        'client_id'      => $client_id,
                        'stripe_user_id' => $stripe_user_id,
                    ),
                    'timeout' => 15,
                )
            );
        }

        // Delete all stored OAuth credentials.
        delete_option( self::OPTION_TEST_ACCESS_TOKEN );
        delete_option( self::OPTION_LIVE_ACCESS_TOKEN );
        delete_option( self::OPTION_STRIPE_USER_ID );
        delete_option( self::OPTION_PUBLISHABLE_KEY );
        delete_option( self::OPTION_LIVEMODE );

        STRP_SUB_Logger::log( 'Stripe OAuth: disconnected.' );

        set_transient( 'strp_sub_oauth_disconnected_' . get_current_user_id(), true, 60 );
        wp_safe_redirect( admin_url( 'admin.php?page=strp-sub-settings' ) );
        exit;
    }

    // â”€â”€ Settings page UI â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€

    /**
     * Render the OAuth connection banner for the settings page.
     * Called from STRP_SUB_Settings::general_section_callback().
     */
    public static function render_connection_section() {
        $is_connected = self::is_connected();
        $client_id    = self::get_client_id();
        $border_color = $is_connected ? '#00a32a' : '#d63638';

        echo '<div style="background:#fff;border:1px solid #c3c4c7;border-left:4px solid ' . esc_attr( $border_color ) . ';padding:20px 24px;margin:16px 0 24px;border-radius:4px;">';

        if ( $is_connected ) {
            $account_id = get_option( self::OPTION_STRIPE_USER_ID, '' );
            $mode_label = STRP_SUB_Settings::is_test_mode()
                ? __( 'Test', 'subscription-manager-for-stripe' )
                : __( 'Live', 'subscription-manager-for-stripe' );

            echo '<h3 style="margin-top:0;color:#00a32a;"><span class="dashicons dashicons-yes-alt" style="vertical-align:middle;margin-right:4px;"></span>' . esc_html__( 'Connected to Stripe', 'subscription-manager-for-stripe' ) . '</h3>';
            echo '<table style="border-collapse:collapse;">';
            echo '<tr><td style="padding:2px 12px 2px 0;font-weight:600;">' . esc_html__( 'Account ID', 'subscription-manager-for-stripe' ) . '</td><td><code>' . esc_html( $account_id ) . '</code></td></tr>';
            echo '<tr><td style="padding:2px 12px 2px 0;font-weight:600;">' . esc_html__( 'Mode', 'subscription-manager-for-stripe' ) . '</td><td>' . esc_html( $mode_label ) . '</td></tr>';
            echo '</table>';

            $disconnect_url = wp_nonce_url(
                admin_url( 'admin-post.php?action=strp_sub_oauth_disconnect' ),
                'strp_sub_oauth_disconnect'
            );
            echo '<p><a href="' . esc_url( $disconnect_url ) . '" class="button button-secondary" onclick="return confirm(\'' . esc_js( __( 'Disconnect your Stripe account?', 'subscription-manager-for-stripe' ) ) . '\');">' . esc_html__( 'Disconnect', 'subscription-manager-for-stripe' ) . '</a></p>';

        } else {
            echo '<h3 style="margin-top:0;"><span class="dashicons dashicons-admin-links" style="vertical-align:middle;margin-right:4px;"></span>' . esc_html__( 'Connect with Stripe (Required)', 'subscription-manager-for-stripe' ) . '</h3>';
            echo '<p>' . esc_html__( 'Stripe now mandates OAuth 2.0 for all third-party plugins. Connecting via OAuth eliminates manual API key management and is required to avoid financial penalties from Stripe starting 2025.', 'subscription-manager-for-stripe' ) . '</p>';

            if ( empty( $client_id ) ) {
                echo '<p style="color:#d63638;"><strong>' . esc_html__( 'Action required:', 'subscription-manager-for-stripe' ) . '</strong> ' . esc_html__( 'Enter your Stripe App\'s Client ID in the field below, save, then click Connect.', 'subscription-manager-for-stripe' ) . '</p>';
                echo '<p class="description">' . wp_kses(
                    sprintf(
                        /* translators: %s: URL to Stripe Connect dashboard */
                        __( 'Obtain a Client ID by registering at %s.', 'subscription-manager-for-stripe' ),
                        '<a href="https://dashboard.stripe.com/settings/connect" target="_blank">' . esc_html__( 'Stripe Dashboard &rarr; Settings &rarr; Connect', 'subscription-manager-for-stripe' ) . '</a>'
                    ),
                    array( 'a' => array( 'href' => array(), 'target' => array() ) )
                ) . '</p>';
            } else {
                $connect_url = wp_nonce_url(
                    admin_url( 'admin-post.php?action=strp_sub_oauth_connect' ),
                    'strp_sub_oauth_connect'
                );
                echo '<a href="' . esc_url( $connect_url ) . '" ';
                echo 'style="display:inline-block;background:#635bff;color:#fff;border:none;border-radius:6px;';
                echo 'padding:10px 22px;font-size:15px;font-weight:600;text-decoration:none;cursor:pointer;">';
                echo '<span class="dashicons dashicons-admin-links" style="vertical-align:middle;margin-right:4px;"></span>' . esc_html__( 'Connect with Stripe', 'subscription-manager-for-stripe' ) . '</a>';
                echo '<p class="description" style="margin-top:10px;"><strong>' . esc_html__( 'Redirect URI', 'subscription-manager-for-stripe' ) . '</strong> ' . esc_html__( '(add this in your Stripe App settings):', 'subscription-manager-for-stripe' ) . '<br>';
                echo '<code>' . esc_url( self::get_redirect_uri() ) . '</code></p>';
            }
        }

        echo '</div>';
    }

    // â”€â”€ Private helpers â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€

    /**
     * Exchange an authorisation code for an access token via Stripe's token endpoint.
     *
     * @param  string           $code The one-time code from Stripe's callback.
     * @return array|WP_Error        Decoded token response or WP_Error on failure.
     */
    private function exchange_code( $code ) {
        $client_secret = self::get_client_secret();
        if ( empty( $client_secret ) ) {
            return new WP_Error( 'no_client_secret', 'A Stripe OAuth Client Secret is required before connecting via OAuth.' );
        }

        $response = wp_remote_post(
            self::TOKEN_URL,
            array(
                'headers' => array(
                    'Authorization' => 'Basic ' . base64_encode( $client_secret . ':' ),
                    'Content-Type'  => 'application/x-www-form-urlencoded',
                ),
                'body'    => array(
                    'grant_type' => 'authorization_code',
                    'code'       => $code,
                ),
                'timeout' => 30,
            )
        );

        if ( is_wp_error( $response ) ) {
            return $response;
        }

        $body        = json_decode( wp_remote_retrieve_body( $response ), true );
        $http_status = (int) wp_remote_retrieve_response_code( $response );

        if ( $http_status !== 200 ) {
            $err  = isset( $body['error'] )             ? $body['error']             : 'unknown_error';
            $desc = isset( $body['error_description'] ) ? $body['error_description'] : '';
            return new WP_Error( $err, $desc );
        }

        return $body;
    }
}
