<?php
/**
 * PostGate Bridge Connection Class
 *
 * This file contains the bridge connection management class that handles
 * communication with the PostGate Bridge server for OAuth authentication
 * and credential management through REST API endpoints.
 *
 * The bridge server acts as a secure intermediary between WordPress sites
 * and Stripe Connect, managing OAuth tokens and providing a simplified
 * connection flow for merchants.
 *
 * @package PostGate
 * @subpackage BridgeIntegration
 * @since 1.0.0
 * @author QodeSphere
 * @license GPL-2.0+
 */

defined( 'ABSPATH' ) || exit;

/**
 * PostGate Bridge Connection Class
 *
 * Manages the connection between the PostGate plugin and the PostGate Bridge
 * server through REST API endpoints. Handles OAuth authentication flow,
 * credential storage, and maintains connection status for both sandbox and live modes.
 *
 * Key Features:
 * - Secure OAuth flow initialization via bridge server
 * - Dual-mode support (sandbox and live environments)
 * - Token claiming and credential storage
 * - Connection status checking
 * - Disconnection and cleanup
 *
 * REST API Endpoints:
 * - POST /postgate/v1/bridge/connect - Start Stripe Connect OAuth flow
 * - GET /postgate/v1/bridge/status - Check connection status
 * - POST /postgate/v1/bridge/disconnect - Disconnect current mode
 * - GET /postgate/v1/stripe/return - Handle OAuth callback from Stripe
 *
 * The bridge server securely manages OAuth credentials and provides
 * endpoints for WordPress sites to authenticate with Stripe via Connect.
 * This architecture keeps sensitive credentials off the WordPress site
 * and provides a streamlined connection experience.
 *
 * @since 1.0.0
 */
class PostGate_Bridge_Connect {

	/**
	 * Initialize the bridge connection class.
	 *
	 * Sets up WordPress REST API routes for bridge communication.
	 * This method should be called during plugin initialization to
	 * register all necessary endpoints for OAuth flow management.
	 *
	 * Registers hooks:
	 * - rest_api_init: Registers REST API routes
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	public static function init(): void {
		add_action( 'rest_api_init', [ __CLASS__, 'register_routes' ] );
	}

	/**
	 * Register REST API routes for bridge communication.
	 *
	 * Registers four endpoints for managing the Stripe Connect integration:
	 * 1. POST /bridge/connect - Initiates OAuth flow
	 * 2. GET /bridge/status - Returns connection status
	 * 3. POST /bridge/disconnect - Removes stored credentials
	 * 4. GET /stripe/return - Handles OAuth callback
	 *
	 * All endpoints except the return callback require manage_options capability
	 * to ensure only administrators can manage Stripe connections.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	public static function register_routes(): void {
		register_rest_route(
			'postgate/v1',
			'/bridge/connect',
			[
				'methods' => 'POST',
				'callback' => [ __CLASS__, 'bridge_start' ],
				'permission_callback' => [ __CLASS__, 'check_permission' ],
			]
		);

		// Stripe Connect: status
		register_rest_route(
			'postgate/v1',
			'/bridge/status',
			[
				'methods' => 'GET',
				'callback' => [ __CLASS__, 'bridge_status' ],
				'permission_callback' => [ __CLASS__, 'check_permission' ],
			]
		);

		// Stripe Connect: disconnect current mode
		register_rest_route(
			'postgate/v1',
			'/bridge/disconnect',
			[
				'methods' => 'POST',
				'callback' => [ __CLASS__, 'bridget_disconnect' ],
				'permission_callback' => [ __CLASS__, 'check_permission' ],
			]
		);

		register_rest_route(
			'postgate/v1',
			'/stripe/return',
			[
				'methods' => 'GET',
				'callback' => [ __CLASS__, 'handle_return' ],
				'permission_callback' => [ __CLASS__, 'validate_oauth_callback' ],
			]
		);
	}

	/**
	 * Check if current user has permission to access bridge endpoints.
	 *
	 * Verifies that the current user has the 'manage_options' capability,
	 * which is required for all bridge/Stripe connection endpoints.
	 *
	 * @since 1.0.0
	 *
	 * @return bool True if user has manage_options capability, false otherwise.
	 */
	public static function check_permission(): bool {
		return current_user_can( 'manage_options' );
	}

	/**
	 * Validate OAuth callback request parameters.
	 *
	 * This permission callback performs early validation of the Stripe OAuth
	 * return callback. It ensures that:
	 * 1. The 'state' parameter contains a valid token
	 * 2. The token corresponds to a valid OAuth session initiated by an admin
	 * 3. The admin user still has manage_options capability
	 * 4. The 'stripe' parameter is present and valid
	 *
	 * The actual decoding and data structure validation happens in handle_return().
	 * This method provides fail-fast validation for obviously malformed requests.
	 *
	 * Security: Uses transient storage with user_id to ensure strong authentication.
	 * When an admin initiates OAuth, we store their user_id in a transient keyed by
	 * a random token. On callback, we validate the token, retrieve the user_id,
	 * verify the user still has admin capabilities, and delete the transient (single-use).
	 *
	 * @since 1.0.0
	 *
	 * @param WP_REST_Request $request The REST request object.
	 *
	 * @return true|WP_Error True if request is valid, WP_Error otherwise.
	 */
	public static function validate_oauth_callback( WP_REST_Request $request ) {
		// Verify state token to prevent CSRF attacks and ensure admin authorization
		$state = $request->get_param( 'state' );

		if ( empty( $state ) ) {
			return new WP_Error(
				'missing_state',
				__( 'Missing state parameter', 'postgate' ),
				[ 'status' => 403 ]
			);
		}

		// Retrieve user_id from transient
		$transient_key = 'postgate_oauth_' . $state;
		$user_id = get_transient( $transient_key );

		if ( false === $user_id ) {
			return new WP_Error(
				'invalid_state',
				__( 'Invalid or expired state parameter', 'postgate' ),
				[ 'status' => 403 ]
			);
		}

		// Verify the user still has admin capabilities
		if ( ! user_can( $user_id, 'manage_options' ) ) {
			delete_transient( $transient_key );
			return new WP_Error(
				'insufficient_permissions',
				__( 'Permission Error: Your user account no longer has administrator privileges. Please log in as an administrator to connect Stripe.', 'postgate' ),
				[ 'status' => 403 ]
			);
		}

		// Delete transient to prevent reuse (single-use token)
		delete_transient( $transient_key );

		$param = $request->get_param( 'stripe' );

		if ( empty( $param ) || ! is_string( $param ) ) {
			return new WP_Error(
				'missing_callback_data',
				__( 'Connection Error: Missing or invalid callback data from Stripe. Please try connecting again. If this problem persists, contact support.', 'postgate' ),
				[ 'status' => 403 ]
			);
		}

		// Validate base64url format (only alphanumeric, dash, and underscore)
		if ( ! preg_match( '/^[A-Za-z0-9_-]+$/', $param ) ) {
			return new WP_Error(
				'invalid_format',
				__( 'Connection Error: Invalid callback format received from Stripe. This may indicate a security issue. Please try again or contact support.', 'postgate' ),
				[ 'status' => 403 ]
			);
		}

		return true;
	}

	/**
	 * Start the Stripe Connect OAuth flow.
	 *
	 * Initiates the OAuth process by:
	 * 1. Generating a unique handshake ID for session tracking
	 * 2. Preparing payload with site URL and return endpoint
	 * 3. Communicating with bridge server to get OAuth URL
	 * 4. Returning the connection URL to the frontend
	 *
	 * The handshake ID is used to securely match the return callback
	 * with this connection attempt and claim the appropriate tokens.
	 *
	 * @since 1.0.0
	 *
	 * @param WP_REST_Request $request The REST request object containing:
	 *                                 - mode: 'live' or 'sandbox' (default: sandbox)
	 *
	 * @return WP_REST_Response|WP_Error Response with connection URL and handshake ID on success,
	 *                                   or WP_Error if bridge communication fails.
	 *                                   Success response structure:
	 *                                   {
	 *                                     "url": "https://connect.stripe.com/...",
	 *                                     "handshakeId": "abc123..."
	 *                                   }
	 */
	public static function bridge_start( WP_REST_Request $request ) {
		$mode = ( $request->get_param( 'mode' ) === 'live' ) ? 'live' : 'sandbox';
		$handshake_id = bin2hex( random_bytes( 16 ) );

		// Generate random token for OAuth state parameter
		$state_token = bin2hex( random_bytes( 16 ) );

		// Store current admin user_id in transient for callback validation
		// This ensures only the admin who initiated OAuth can complete it
		$transient_key = 'postgate_oauth_' . $state_token;
		set_transient( $transient_key, get_current_user_id(), HOUR_IN_SECONDS );

		// Build return URL with state parameter for CSRF protection and user validation
		$return_url = add_query_arg(
			'state',
			$state_token,
			rest_url( 'postgate/v1/stripe/return' )
		);

		$payload = [
			'mode' => $mode,
			'site_url' => home_url( '/' ),
			'return_url' => $return_url,
			'handshake_id' => $handshake_id,
		];

		$bridge_response = PostGate_Bridge_Signer::post( '/connect', $payload );

		if ( is_wp_error( $bridge_response ) || empty( $bridge_response['url'] ) ) {
			return new WP_Error(
				'connect_failed',
				__( 'Could not start Stripe Connect.', 'postgate' ),
				is_wp_error( $bridge_response ) ? $bridge_response->get_error_data() : []
			);
		}

		return new WP_REST_Response(
			[
				'url' => (string) $bridge_response['url'],
				'handshakeId' => $handshake_id,
			],
			200
		);
	}

	/**
	 * Handle the OAuth return callback from Stripe.
	 *
	 * Processes the return from Stripe Connect with the following flow:
	 * 1. Decode and validate base64url encoded callback data
	 * 2. Extract mode, account ID, and handshake ID
	 * 3. Store the connected account ID
	 * 4. Claim access tokens from bridge server using handshake ID
	 * 5. Store all credentials (access, refresh, publishable keys) for both modes
	 * 6. Redirect to settings page with success/failure notice
	 *
	 * The callback data is base64url encoded to safely pass through URL parameters.
	 * Both live and sandbox credentials may be stored if the merchant has both
	 * modes available in their Stripe account.
	 *
	 * @since 1.0.0
	 *
	 * @param WP_REST_Request $request The REST request object containing:
	 *                                 - stripe: Base64url encoded JSON with connection data
	 *                                   Expected structure after decoding:
	 *                                   {
	 *                                     "mode": "live|sandbox",
	 *                                     "connected_account_id": "acct_...",
	 *                                     "handshake_id": "abc123..."
	 *                                   }
	 *
	 * @return WP_REST_Response Response with appropriate HTTP status (redirects on success/failure).
	 *                          Returns 400 for invalid/missing data.
	 */
	public static function handle_return( WP_REST_Request $request ): WP_REST_Response {
		$param = (string) $request->get_param( 'stripe' );
		if ( empty( $param ) ) {
			return new WP_REST_Response( 'Missing data', 400 );
		}

		$json = json_decode( self::decode_base64url( $param ), true );
		if ( ! is_array( $json ) ) {
			return new WP_REST_Response( 'Bad data', 400 );
		}

		$mode = ( ( $json['mode'] ?? '' ) === 'live' ) ? 'live' : 'sandbox';
		$account_id = (string) ( $json['connected_account_id'] ?? '' );
		$handshake_id = (string) ( $json['handshake_id'] ?? '' );

		if ( ! $account_id || ! $handshake_id ) {
			return new WP_REST_Response( 'Invalid payload', 400 );
		}

		// Store the connected account for the specific mode
		update_option( "postgate_connected_acct_{$mode}", $account_id, false );

		// Claim the access tokens from the bridge server
		$claim = PostGate_Bridge_Signer::post(
			'/token/claim',
			[
				'mode' => $mode,
				'handshake_id' => $handshake_id,
			]
		);

		if ( is_wp_error( $claim ) || empty( $claim['stripe_user_id'] ) ) {
			wp_safe_redirect( admin_url( 'admin.php?page=postgate-settings&pg_notice=claim_failed#payment' ) );
			exit;
		}

		// Store the connected account ID for the specific mode
		update_option( "postgate_connected_acct_{$mode}", (string) $claim['stripe_user_id'], false );

		// Store access tokens for both modes if available
		if ( ! empty( $claim['live']['access_token'] ) ) {
			update_option( 'postgate_access_token_live', (string) $claim['live']['access_token'], false );
		}
		if ( ! empty( $claim['live']['refresh_token'] ) ) {
			update_option( 'postgate_refresh_token_live', (string) $claim['live']['refresh_token'], false );
		}
		if ( ! empty( $claim['sandbox']['access_token'] ) ) {
			update_option( 'postgate_access_token_sandbox', (string) $claim['sandbox']['access_token'], false );
		}
		if ( ! empty( $claim['sandbox']['refresh_token'] ) ) {
			update_option( 'postgate_refresh_token_sandbox', (string) $claim['sandbox']['refresh_token'], false );
		}
		if ( ! empty( $claim['live']['publishable_key'] ) ) {
			update_option( 'postgate_publishable_key_live', (string) $claim['live']['publishable_key'], false );
		}
		if ( ! empty( $claim['sandbox']['publishable_key'] ) ) {
			update_option( 'postgate_publishable_key_sandbox', (string) $claim['sandbox']['publishable_key'], false );
		}

		wp_safe_redirect( admin_url( 'admin.php?page=postgate-settings&pg_notice=connected#payment' ) );
		exit;
	}
	/**
	 * Disconnect the current site from Stripe for a given mode.
	 *
	 * Removes stored credentials and related settings for the specified mode
	 * by delegating to the Merchant Auth service. This clears:
	 * - Access tokens
	 * - Refresh tokens
	 * - Publishable keys
	 * - Connected account ID
	 *
	 * The disconnection only affects the specified mode (live or sandbox),
	 * leaving the other mode's credentials intact if both are connected.
	 *
	 * @since 1.0.0
	 *
	 * @param WP_REST_Request $request The REST request containing:
	 *                                 - mode: 'live' or 'sandbox' (default: sandbox)
	 *
	 * @return WP_REST_Response The response indicating success and affected mode.
	 *                          Response structure:
	 *                          {
	 *                            "ok": true,
	 *                            "mode": "live|sandbox"
	 *                          }
	 */
	public static function bridget_disconnect( WP_REST_Request $request ): WP_REST_Response {
		$mode = ( $request->get_param( 'mode' ) === 'live' ) ? 'live' : 'sandbox';

		// Delegate to centralized Merchant Auth disconnect method
		PostGate_Merchant_Auth::disconnect( $mode );

		return rest_ensure_response(
			[
				'ok' => true,
				'mode' => $mode,
			]
		);
	}

	/**
	 * Provide Stripe connection status for a given mode.
	 *
	 * Returns whether the site is connected to Stripe and, if so, provides
	 * the connected account ID. This endpoint is used by the admin interface
	 * to display connection status and enable/disable relevant features.
	 *
	 * Checks both the Merchant Auth service and legacy option storage
	 * to ensure backwards compatibility with older plugin versions.
	 *
	 * @since 1.0.0
	 *
	 * @param WP_REST_Request $request The REST request which may include:
	 *                                 - mode: 'live' or 'sandbox' (default: sandbox)
	 *
	 * @return WP_REST_Response The status payload with connection information.
	 *                          Response structure:
	 *                          {
	 *                            "mode": "live|sandbox",
	 *                            "connected": true|false,
	 *                            "account_id": "acct_..." or ""
	 *                          }
	 */
	public static function bridge_status( WP_REST_Request $request ): WP_REST_Response {
		$mode = ( $request->get_param( 'mode' ) === 'live' ) ? 'live' : 'sandbox';

		// Retrieves the merchant account ID for the specified mode.
		$account_id = (string) PostGate_Merchant_Auth::account( $mode );
		if ( ! $account_id ) {
			$account_id = (string) get_option( "postgate_connected_acct_{$mode}", '' );
		}

		return rest_ensure_response(
			[
				'mode' => $mode,
				'connected' => ! empty( $account_id ),
				'account_id' => $account_id,
			]
		);
	}

	/**
	 * Decode base64url encoded data.
	 *
	 * Helper method to decode base64url encoded data from Stripe callback.
	 * Base64url is a URL-safe variant of base64 encoding that replaces
	 * '+' with '-' and '/' with '_', and may omit padding '=' characters.
	 *
	 * This method:
	 * 1. Adds back padding if needed (length must be multiple of 4)
	 * 2. Converts URL-safe characters back to standard base64
	 * 3. Decodes the base64 string
	 *
	 * @since 1.0.0
	 *
	 * @param string $data The base64url encoded data string.
	 *
	 * @return string The decoded data as a plain string (typically JSON).
	 */
	private static function decode_base64url( string $data ): string {
		$remainder = strlen( $data ) % 4;
		if ( $remainder ) {
			$data .= str_repeat( '=', 4 - $remainder );
		}
		return base64_decode( strtr( $data, '-_', '+/' ) );
	}

}