<?php
/**
 * 2Checkout / Verifone REST API client (API 6.0).
 *
 * Server-side only. Authenticates with an HMAC of Merchant Code + Secret Key per request.
 * See https://docs.2checkout.com/ — "API authentication".
 *
 * @package BW_2Checkout_Pricing
 */

defined( 'ABSPATH' ) || exit;

/**
 * Thin, cached REST client. Uses wp_remote_* only (TLS verification stays on).
 */
class BW_2Checkout_Pricing_Client {

	/** @var string */
	protected $merchant_code;

	/** @var string */
	protected $secret_key;

	/** @var string */
	protected $api_base;

	/** @var int */
	protected $cache_ttl;

	public function __construct() {
		$this->merchant_code = bw_2checkout_pricing_get_merchant_code();
		$this->secret_key    = bw_2checkout_pricing_get_secret_key();
		$this->api_base      = trailingslashit( (string) bw_2checkout_pricing_get_setting( 'api_base', 'https://api.2checkout.com/rest/6.0/' ) );
		$this->cache_ttl     = (int) bw_2checkout_pricing_get_setting( 'cache_ttl', 3600 );
	}

	/**
	 * Whether credentials are present. Callers should check before making requests.
	 *
	 * @return bool
	 */
	public function is_configured() {
		return '' !== $this->merchant_code && '' !== $this->secret_key;
	}

	/**
	 * Build the X-Avangate-Authentication header value.
	 *
	 * Hash string = strlen(code) . code . strlen(date) . date  (date in UTC, 'Y-m-d H:i:s'),
	 * HMAC-SHA256 keyed with the Secret Key. Verify end-to-end against the sandbox in Step 1.
	 *
	 * @return string
	 */
	protected function auth_header_value() {
		$date   = gmdate( 'Y-m-d H:i:s' );
		$string = strlen( $this->merchant_code ) . $this->merchant_code . strlen( $date ) . $date;
		$hash   = hash_hmac( 'sha256', $string, $this->secret_key );

		return sprintf(
			'code="%s" date="%s" hash="%s" algo="sha256"',
			$this->merchant_code,
			$date,
			$hash
		);
	}

	/**
	 * Perform a request against the REST API.
	 *
	 * @param string $method HTTP method.
	 * @param string $path   Path relative to the API base (e.g. 'products/').
	 * @param array  $body   Optional body for write methods.
	 * @return array|WP_Error Decoded JSON as an associative array, or WP_Error.
	 */
	public function request( $method, $path, $body = null ) {
		if ( ! $this->is_configured() ) {
			return new WP_Error( 'bw_2checkout_pricing_not_configured', __( '2Checkout credentials are not configured.', 'bw-2checkout-pricing' ) );
		}

		$url  = $this->api_base . ltrim( $path, '/' );
		$args = array(
			'method'  => strtoupper( $method ),
			'timeout' => 20,
			'headers' => array(
				'X-Avangate-Authentication' => $this->auth_header_value(),
				'Content-Type'              => 'application/json',
				'Accept'                    => 'application/json',
			),
		);

		if ( null !== $body ) {
			$args['body'] = wp_json_encode( $body );
		}

		$response = wp_remote_request( $url, $args );

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

		$code = (int) wp_remote_retrieve_response_code( $response );
		$data = json_decode( wp_remote_retrieve_body( $response ), true );

		if ( $code < 200 || $code >= 300 ) {
			return new WP_Error(
				'bw_2checkout_pricing_http_' . $code,
				sprintf(
					/* translators: %d: HTTP status code */
					__( '2Checkout API returned HTTP %d.', 'bw-2checkout-pricing' ),
					$code
				),
				$data
			);
		}

		return is_array( $data ) ? $data : array();
	}

	/**
	 * List all products (with product codes). Cached.
	 *
	 * @return array|WP_Error
	 */
	public function get_products() {
		$key    = 'bw_2checkout_pricing_products';
		$cached = get_transient( $key );
		if ( false !== $cached ) {
			return $cached;
		}

		$result = $this->request( 'GET', 'products/' );
		if ( ! is_wp_error( $result ) ) {
			set_transient( $key, $result, $this->cache_ttl );
		}
		return $result;
	}

	/**
	 * Retrieve a product's pricing configurations (currencies, per-country configs, option prices).
	 * This is the source of truth for the widget's displayed prices. Cached.
	 *
	 * @param string $product_code 2Checkout product code.
	 * @return array|WP_Error
	 */
	public function get_pricing_configurations( $product_code ) {
		$product_code = (string) $product_code;
		if ( '' === $product_code ) {
			return new WP_Error( 'bw_2checkout_pricing_bad_product', __( 'Missing product code.', 'bw-2checkout-pricing' ) );
		}

		$key    = 'bw_2checkout_pricing_pc_' . md5( $product_code );
		$cached = get_transient( $key );
		if ( false !== $cached ) {
			return $cached;
		}

		$result = $this->request( 'GET', 'products/' . rawurlencode( $product_code ) . '/pricingconfigurations/' );
		if ( ! is_wp_error( $result ) ) {
			set_transient( $key, $result, $this->cache_ttl );
		}
		return $result;
	}

	/**
	 * Order-preview (PUT /orders/0/) to get the converted UNIT net price of a product in a given
	 * currency. Used to derive the live FX multiplier for non-USD display. Cached; the cache key
	 * ignores the "users" option because the base unit price it returns is users-independent.
	 *
	 * @param string $product_code  2Checkout product code.
	 * @param array  $option_values Map of option-group code => selected value.
	 * @param string $currency      ISO 4217 code.
	 * @return float|WP_Error Unit net price in the requested currency.
	 */
	public function order_preview_unit_net( $product_code, array $option_values, $currency ) {
		$product_code = (string) $product_code;
		$currency     = strtoupper( (string) $currency );
		if ( '' === $product_code ) {
			return new WP_Error( 'bw_2checkout_pricing_bad_product', __( 'Missing product code.', 'bw-2checkout-pricing' ) );
		}

		$ck = $option_values;
		unset( $ck['users'] );
		ksort( $ck );
		$key    = 'bw_2checkout_pricing_prev_' . md5( $product_code . '|' . $currency . '|' . wp_json_encode( $ck ) );
		$cached = get_transient( $key );
		if ( false !== $cached ) {
			return $cached;
		}

		$price_options = array();
		foreach ( $option_values as $name => $value ) {
			$price_options[] = array(
				'Name'    => (string) $name,
				'Options' => array( array( 'Value' => (string) $value ) ),
			);
		}

		$body = array(
			'Currency' => $currency,
			'Items'    => array(
				array(
					'Code'         => $product_code,
					'Quantity'     => 1,
					'PriceOptions' => $price_options,
				),
			),
		);

		$res = $this->request( 'PUT', 'orders/0/', $body );
		if ( is_wp_error( $res ) ) {
			return $res;
		}

		$unit = isset( $res['Items'][0]['Price']['UnitNetPrice'] ) ? $res['Items'][0]['Price']['UnitNetPrice'] : null;
		if ( ! is_numeric( $unit ) ) {
			return new WP_Error( 'bw_2checkout_pricing_no_unit', __( 'Order preview returned no unit price.', 'bw-2checkout-pricing' ) );
		}

		$unit = (float) $unit;
		set_transient( $key, $unit, $this->cache_ttl );
		return $unit;
	}

	/**
	 * Clear all cached API responses (call after a settings save).
	 *
	 * @return void
	 */
	public static function flush_cache() {
		global $wpdb;
		$wpdb->query(
			$wpdb->prepare(
				"DELETE FROM {$wpdb->options} WHERE option_name LIKE %s OR option_name LIKE %s",
				$wpdb->esc_like( '_transient_bw_2checkout_pricing_' ) . '%',
				$wpdb->esc_like( '_transient_timeout_bw_2checkout_pricing_' ) . '%'
			)
		);
	}
}
