<?php
/**
 * PeachPay Stripe HTTP utility class.
 *
 * Thin wrapper around the WordPress HTTP API for making Stripe REST API requests.
 * Replaces the stripe/stripe-php SDK for all direct API communication.
 *
 * Usage:
 *   $result = PeachPay_Stripe_HTTP::post( '/v1/payment_intents', $params, $mode );
 *   if ( is_wp_error( $result ) ) { ... }
 *   $id = $result['id'];
 *
 * @package PeachPay
 */

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

/**
 * Provides static GET and POST helpers for the Stripe REST API.
 *
 * All methods return a decoded associative array on success or a WP_Error
 * on any failure (network error, non-2xx HTTP status, or invalid JSON).
 * Callers must check is_wp_error() before accessing the returned value.
 */
final class PeachPay_Stripe_HTTP {

	/**
	 * Stripe REST API base URL.
	 */
	const API_BASE = 'https://api.stripe.com';

	/**
	 * Stripe API version.
	 */
	const STRIPE_API_VERSION = '2025-12-15.clover';

	/**
	 * Default request timeout in seconds.
	 */
	const TIMEOUT = 30;

	/**
	 * Makes a GET request to the Stripe API.
	 *
	 * @param string $path   Stripe API path, e.g. '/v1/payment_methods/pm_xxx'.
	 * @param string $mode   'live' or 'test'. Defaults to current site mode when null.
	 * @param array  $params Optional query-string parameters.
	 * @return array|WP_Error Decoded JSON array on success, WP_Error on failure.
	 */
	public static function get( $path, $mode = null, $params = array() ) {
		$url = self::API_BASE . $path;

		if ( ! empty( $params ) ) {
			// Stripe uses bracket notation for nested params (e.g. expand[0]=balance_transaction).
			$url = add_query_arg( self::flatten_params( $params ), $url );
		}

		$response = wp_remote_get(
			$url,
			array(
				'headers' => self::build_headers( $mode ),
				'timeout' => self::TIMEOUT,
			)
		);

		return self::parse_response( $response );
	}

	/**
	 * Makes a GET request to the Stripe API using an explicit secret key.
	 *
	 * Use this variant when the key is not yet stored in site options — for example
	 * during credential verification before they are saved for the first time.
	 *
	 * @param string $path       Stripe API path, e.g. '/v1/account'.
	 * @param string $secret_key Raw Stripe secret key (sk_live_… or sk_test_…).
	 * @param array  $params     Optional query-string parameters.
	 * @return array|WP_Error Decoded JSON array on success, WP_Error on failure.
	 */
	public static function get_with_key( $path, $secret_key, $params = array() ) {
		$url = self::API_BASE . $path;

		if ( ! empty( $params ) ) {
			$url = add_query_arg( self::flatten_params( $params ), $url );
		}

		$response = wp_remote_get(
			$url,
			array(
				'headers' => self::build_headers( null, $secret_key ),
				'timeout' => self::TIMEOUT,
			)
		);

		return self::parse_response( $response );
	}

	/**
	 * Makes a POST request to the Stripe API.
	 *
	 * @param string $path            Stripe API path, e.g. '/v1/payment_intents'.
	 * @param array  $body            Request body parameters (form-encoded).
	 * @param string $mode            'live' or 'test'. Defaults to current site mode when null.
	 * @param string $idempotency_key Optional Idempotency-Key header value.
	 * @return array|WP_Error Decoded JSON array on success, WP_Error on failure.
	 */
	public static function post( $path, $body = array(), $mode = null, $idempotency_key = '' ) {
		$headers = self::build_headers( $mode );

		if ( ! empty( $idempotency_key ) ) {
			$headers['Idempotency-Key'] = $idempotency_key;
		}

		$response = wp_remote_post(
			self::API_BASE . $path,
			array(
				'headers' => $headers,
				'body'    => http_build_query( self::flatten_params( $body ) ),
				'timeout' => self::TIMEOUT,
			)
		);

		return self::parse_response( $response );
	}

	// -------------------------------------------------------------------------
	// Private helpers
	// -------------------------------------------------------------------------

	/**
	 * Builds the common HTTP headers required for every Stripe API request.
	 *
	 * Auth uses the Bearer token pattern documented at
	 * https://stripe.com/docs/api/authentication.
	 *
	 * @param string|null $mode       'live' or 'test'. Null resolves to the current site mode.
	 *                                Ignored when $secret_key is supplied explicitly.
	 * @param string|null $secret_key If provided, use this key directly instead of fetching
	 *                                from stored options. Intended for pre-save verification.
	 * @return array Key/value header array.
	 */
	private static function build_headers( $mode = null, $secret_key = null ) {
		if ( null === $secret_key ) {
			$secret_key = PeachPay_Stripe_Credentials::get_secret_key( $mode );
		}

		return array(
			'Authorization'  => 'Bearer ' . $secret_key,
			'Stripe-Version' => self::STRIPE_API_VERSION,
			'Content-Type'   => 'application/x-www-form-urlencoded',
		);
	}

	/**
	 * Parses a WordPress HTTP API response into a data array or WP_Error.
	 *
	 * Error codes returned in the WP_Error data array:
	 *   'code'        — HTTP status code (int)
	 *   'stripe_code' — Stripe error.code string, e.g. 'card_declined' (string)
	 *
	 * @param array|WP_Error $response Raw WordPress HTTP response.
	 * @return array|WP_Error Decoded response array or WP_Error on failure.
	 */
	private static function parse_response( $response ) {
		if ( is_wp_error( $response ) ) {
			return $response;
		}

		$response_code = wp_remote_retrieve_response_code( $response );
		$body          = wp_remote_retrieve_body( $response );
		$data          = json_decode( $body, true );

		if ( $response_code < 200 || $response_code >= 300 ) {
			$message     = isset( $data['error']['message'] )
				? $data['error']['message']
				: __( 'Unknown Stripe API error.', 'peachpay-for-woocommerce' );
			$stripe_code = isset( $data['error']['code'] ) ? $data['error']['code'] : '';

			return new WP_Error(
				'stripe_api_error',
				$message,
				array(
					'code'        => $response_code,
					'stripe_code' => $stripe_code,
				)
			);
		}

		if ( ! is_array( $data ) ) {
			return new WP_Error(
				'stripe_invalid_response',
				__( 'Could not decode Stripe API response.', 'peachpay-for-woocommerce' )
			);
		}

		return $data;
	}

	/**
	 * Flattens a nested params array into Stripe's bracket notation.
	 *
	 * Stripe's REST API encodes nested structures as bracket-notation strings.
	 * For example:
	 *   array( 'expand' => array( 'balance_transaction' ) )
	 *   → array( 'expand[0]' => 'balance_transaction' )
	 *
	 *   array( 'metadata' => array( 'order_id' => '123' ) )
	 *   → array( 'metadata[order_id]' => '123' )
	 *
	 * PHP booleans are converted to the strings 'true'/'false' because
	 * http_build_query() encodes true as '1' and false as '', both of which
	 * Stripe rejects with "Invalid boolean".
	 *
	 * @param array  $params The parameters to flatten.
	 * @param string $prefix The key prefix used during recursion (leave empty for top level).
	 * @return array Flattened key => value array suitable for http_build_query().
	 */
	private static function flatten_params( $params, $prefix = '' ) {
		$result = array();
		foreach ( $params as $key => $value ) {
			$full_key = $prefix ? "{$prefix}[{$key}]" : $key;
			if ( is_array( $value ) ) {
				$result = array_merge( $result, self::flatten_params( $value, $full_key ) );
			} elseif ( is_bool( $value ) ) {
				// Stripe requires the literal strings 'true'/'false', not PHP's '1'/''.
				$result[ $full_key ] = $value ? 'true' : 'false';
			} else {
				$result[ $full_key ] = $value;
			}
		}
		return $result;
	}
}
