<?php
/**
 * BarnetPOS API client.
 *
 * Security notes, deliberate and load-bearing:
 *  - Barnet's documentation specifies http:// for every endpoint. HTTPS is
 *    available and verified working, but the server does NOT redirect, so a
 *    plain-http URL silently succeeds while sending Basic-auth credentials and
 *    customer PII in cleartext. enforce_https() below refuses to make the
 *    request at all rather than allow that. Do not "fix" it by relaxing this.
 *  - All calls are server-side. The API key must never reach the browser, so
 *    nothing here is exposed over AJAX to non-privileged users or to the front end.
 */

defined( 'ABSPATH' ) || exit;

class BW_Barnet_Client {

	const DEFAULT_BASE  = 'https://barnetnetwork.com/api';
	const OPT_KEY       = 'bw_barnet_api_key';
	const OPT_PASS      = 'bw_barnet_api_pass';
	const OPT_BASE      = 'bw_barnet_base_url';
	const OPT_ACCOUNT   = 'bw_barnet_account_id';
	const OPT_SHOP      = 'bw_barnet_shop_id';
	const TIMEOUT       = 30;

	/**
	 * Resolve credentials. Constants defined in wp-config.php always win over
	 * anything stored in the database, so a hardened install can keep secrets
	 * out of wp_options entirely.
	 *
	 * @return array{key:string,pass:string,source:string}
	 */
	public static function credentials(): array {
		if ( defined( 'BARNET_API_KEY' ) && defined( 'BARNET_API_PASS' ) && BARNET_API_KEY && BARNET_API_PASS ) {
			return array(
				'key'    => (string) BARNET_API_KEY,
				'pass'   => (string) BARNET_API_PASS,
				'source' => 'constants',
			);
		}

		return array(
			'key'    => (string) get_option( self::OPT_KEY, '' ),
			'pass'   => (string) get_option( self::OPT_PASS, '' ),
			'source' => 'database',
		);
	}

	public static function has_credentials(): bool {
		$c = self::credentials();
		return '' !== $c['key'] && '' !== $c['pass'];
	}

	public static function base_url(): string {
		$base = (string) get_option( self::OPT_BASE, self::DEFAULT_BASE );
		return untrailingslashit( $base ?: self::DEFAULT_BASE );
	}

	public static function account_id(): string {
		return (string) get_option( self::OPT_ACCOUNT, '' );
	}

	public static function shop_id(): string {
		return (string) get_option( self::OPT_SHOP, '' );
	}

	/**
	 * Hard refusal to transmit credentials over anything but TLS.
	 *
	 * @return true|WP_Error
	 */
	private static function enforce_https( string $url ) {
		$scheme = strtolower( (string) wp_parse_url( $url, PHP_URL_SCHEME ) );
		if ( 'https' !== $scheme ) {
			return new WP_Error(
				'bw_barnet_insecure_scheme',
				sprintf(
					/* translators: %s: URL scheme found, e.g. "http" */
					__( 'Refusing to send API credentials over "%s". The Barnet base URL must use https. Barnet\'s documentation says http, but https works and http would transmit your API key and customer data in cleartext.', 'bw-barnet' ),
					$scheme ?: 'an empty scheme'
				)
			);
		}
		return true;
	}

	/**
	 * Perform an authenticated request.
	 *
	 * @param string $path  Path below the base URI, e.g. "store/products".
	 * @param array  $query Query parameters.
	 * @return array|WP_Error Decoded JSON body on success.
	 */
	public static function request( string $path, array $query = array() ) {
		$creds = self::credentials();
		if ( '' === $creds['key'] || '' === $creds['pass'] ) {
			return new WP_Error( 'bw_barnet_no_credentials', __( 'Barnet API credentials are not configured.', 'bw-barnet' ) );
		}

		$url = self::base_url() . '/' . ltrim( $path, '/' );

		$secure = self::enforce_https( $url );
		if ( is_wp_error( $secure ) ) {
			return $secure;
		}

		if ( $query ) {
			$url = add_query_arg( array_map( 'rawurlencode', array_map( 'strval', $query ) ), $url );
		}

		$response = wp_remote_get(
			$url,
			array(
				'timeout'     => self::TIMEOUT,
				'redirection' => 0, // Never follow a redirect that could downgrade to http.
				'sslverify'   => true,
				'headers'     => array(
					'Authorization' => 'Basic ' . base64_encode( $creds['key'] . ':' . $creds['pass'] ),
					'Accept'        => 'application/json',
				),
				'user-agent'  => 'BW-Barnet-Connector/' . BW_BARNET_VERSION,
			)
		);

		if ( is_wp_error( $response ) ) {
			// The WP_Error message may contain the URL but never the credentials.
			return $response;
		}

		$code = (int) wp_remote_retrieve_response_code( $response );
		$body = (string) wp_remote_retrieve_body( $response );

		if ( 401 === $code ) {
			return new WP_Error( 'bw_barnet_unauthorized', __( 'Barnet rejected the credentials (401 Unauthorized). Check the API key and password.', 'bw-barnet' ) );
		}

		if ( 429 === $code ) {
			return new WP_Error( 'bw_barnet_rate_limited', __( 'Barnet returned 429 Too Many Requests. A rate limit exists after all; slow the sync down.', 'bw-barnet' ) );
		}

		if ( $code < 200 || $code >= 300 ) {
			return new WP_Error(
				'bw_barnet_http_' . $code,
				sprintf(
					/* translators: 1: HTTP status code, 2: truncated response body */
					__( 'Barnet returned HTTP %1$d. Response began: %2$s', 'bw-barnet' ),
					$code,
					esc_html( substr( wp_strip_all_tags( $body ), 0, 200 ) )
				)
			);
		}

		$decoded = json_decode( $body, true );
		if ( null === $decoded && JSON_ERROR_NONE !== json_last_error() ) {
			return new WP_Error(
				'bw_barnet_bad_json',
				sprintf(
					/* translators: %s: JSON error message */
					__( 'Barnet returned a response that is not valid JSON (%s).', 'bw-barnet' ),
					json_last_error_msg()
				)
			);
		}

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

	/**
	 * One page of the product catalogue.
	 *
	 * Note: store/products exposes no "modified since" parameter, so there is no
	 * incremental option here. Every refresh is a full paginated crawl.
	 *
	 * @param int       $page        1-indexed page number.
	 * @param int       $per_page    Items per page.
	 * @param bool|null $show_on_web Restrict to web-sellable items when true.
	 */
	public static function get_products( int $page = 1, int $per_page = 100, ?bool $show_on_web = true ) {
		$query = array(
			'p'             => max( 1, $page ),
			'items_on_page' => max( 1, $per_page ),
		);
		if ( null !== $show_on_web ) {
			$query['show_on_web'] = $show_on_web ? 'true' : 'false';
		}
		return self::request( 'store/products', $query );
	}

	public static function get_product( string $id ) {
		return self::request( 'store/products/' . rawurlencode( $id ) );
	}

	public static function get_discount_schedule( int $page = 1, int $per_page = 250 ) {
		return self::request(
			'store/discounts_schedule',
			array(
				'p'             => max( 1, $page ),
				'items_on_page' => max( 1, $per_page ),
			)
		);
	}

	/**
	 * Cheapest possible authenticated call, used by the "Test connection" button.
	 *
	 * @return array|WP_Error
	 */
	public static function test_connection() {
		$result = self::get_products( 1, 1, null );
		if ( is_wp_error( $result ) ) {
			return $result;
		}

		$paginator = $result['paginator'] ?? array();

		return array(
			'items_count' => isset( $paginator['items_count'] ) ? (int) $paginator['items_count'] : null,
			'pages'       => isset( $paginator['pages'] ) ? (int) $paginator['pages'] : null,
			'sample_keys' => isset( $result['items'][0] ) && is_array( $result['items'][0] )
				? array_keys( $result['items'][0] )
				: array(),
		);
	}
}
