<?php
/**
 * Cloud API client.
 *
 * @package SubmissionInbox
 */

namespace SUBIN\Cloud;

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

/**
 * Sends JSON requests to the Submission Inbox Cloud plugin API.
 */
class CloudApiClient {
	/**
	 * API base URL.
	 *
	 * @var string
	 */
	private $base_url;

	/**
	 * Bearer/HMAC token.
	 *
	 * @var string
	 */
	private $site_token;

	/**
	 * Constructor.
	 *
	 * @param string $base_url API base URL.
	 * @param string $site_token Site token.
	 */
	public function __construct( $base_url, $site_token = '' ) {
		$this->base_url   = untrailingslashit( esc_url_raw( (string) $base_url ) );
		$this->site_token = (string) $site_token;
	}

	/**
	 * Register a site.
	 *
	 * @param array<string,mixed> $payload Payload.
	 * @return array<string,mixed>|\WP_Error
	 */
	public function register_site( array $payload ) {
		return $this->post( '/connect/register', $payload, false );
	}

	/**
	 * Claim a Cloud-created site connection key.
	 *
	 * @param array<string,mixed> $payload Payload.
	 * @return array<string,mixed>|\WP_Error
	 */
	public function claim_site( array $payload ) {
		return $this->post( '/connect/claim', $payload, false );
	}

	/**
	 * Validate current site connection.
	 *
	 * @param array<string,mixed> $payload Heartbeat payload.
	 *
	 * @return array<string,mixed>|\WP_Error
	 */
	public function validate_connection( array $payload ) {
		return $this->post( '/health/heartbeat', $payload, true );
	}

	/**
	 * Send heartbeat.
	 *
	 * @param array<string,mixed> $payload Payload.
	 * @return array<string,mixed>|\WP_Error
	 */
	public function heartbeat( array $payload ) {
		return $this->post( '/health/heartbeat', $payload, true );
	}

	/**
	 * Send sync events.
	 *
	 * @param array<string,mixed> $payload Payload.
	 * @return array<string,mixed>|\WP_Error
	 */
	public function sync_events( array $payload ) {
		return $this->post( '/sync/events', $payload, true );
	}

	/**
	 * Rotate token.
	 *
	 * @return array<string,mixed>|\WP_Error
	 */
	public function rotate_token() {
		return $this->post( '/tokens/rotate', array(), true );
	}

	/**
	 * Revoke token.
	 *
	 * @return array<string,mixed>|\WP_Error
	 */
	public function revoke_token() {
		return $this->post( '/tokens/revoke', array(), true );
	}

	/**
	 * POST JSON to Cloud.
	 *
	 * @param string              $path API path.
	 * @param array<string,mixed> $payload Payload.
	 * @param bool                $protected Whether bearer token is required.
	 * @return array<string,mixed>|\WP_Error
	 */
	private function post( $path, array $payload, $protected ) {
		if ( '' === $this->base_url ) {
			return new \WP_Error( 'subin_cloud_missing_endpoint', __( 'Cloud API endpoint is missing.', 'submission-inbox' ) );
		}

		if ( $protected && '' === $this->site_token ) {
			return new \WP_Error( 'subin_cloud_missing_token', __( 'Cloud site token is missing.', 'submission-inbox' ) );
		}

		$url  = $this->base_url . $path;
		if ( ! $this->is_safe_endpoint( $url ) ) {
			return new \WP_Error( 'subin_cloud_insecure_endpoint', __( 'Cloud API endpoint must use HTTPS unless it is a local development endpoint.', 'submission-inbox' ) );
		}

		$body = wp_json_encode( $payload );
		if ( ! is_string( $body ) ) {
			return new \WP_Error( 'subin_cloud_invalid_payload', __( 'Cloud request payload could not be encoded.', 'submission-inbox' ) );
		}

		$headers = array(
			'Content-Type' => 'application/json',
		);

		if ( $protected ) {
			$headers['Authorization'] = 'Bearer ' . $this->site_token;
			$headers                  = array_merge( $headers, $this->hmac_headers( 'POST', $url, $body ) );
		}

		$response = wp_remote_post(
			$url,
			array(
				'timeout' => 20,
				'headers' => $headers,
				'body'    => $body,
			)
		);

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

		$status = absint( wp_remote_retrieve_response_code( $response ) );
		$data   = json_decode( (string) wp_remote_retrieve_body( $response ), true );
		if ( ! is_array( $data ) ) {
			$data = array();
		}

		if ( $status < 200 || $status >= 300 ) {
			return new \WP_Error(
				$this->error_code( $data, 'subin_cloud_http_error' ),
				$this->error_message( $data, $status ),
				array(
					'status' => $status,
					'body'   => $data,
					'fields' => $this->error_fields( $data ),
				)
			);
		}

		if ( array_key_exists( 'success', $data ) ) {
			if ( ! rest_sanitize_boolean( $data['success'] ) ) {
				return new \WP_Error(
					$this->error_code( $data, 'subin_cloud_api_error' ),
					$this->error_message( $data, $status ),
					array(
						'status' => $status,
						'body'   => $data,
						'fields' => $this->error_fields( $data ),
					)
				);
			}

			return is_array( $data['data'] ?? null ) ? $data['data'] : array();
		}

		return $data;
	}

	/**
	 * Build optional HMAC headers.
	 *
	 * @param string $method HTTP method.
	 * @param string $url URL.
	 * @param string $body Raw body.
	 * @return array<string,string>
	 */
	private function hmac_headers( $method, $url, $body ) {
		$path      = (string) wp_parse_url( $url, PHP_URL_PATH );
		$timestamp = (string) time();
		$nonce     = wp_generate_uuid4();
		$message   = strtoupper( (string) $method ) . "\n" . $path . "\n" . $timestamp . "\n" . $nonce . "\n" . hash( 'sha256', $body );

		return array(
			'X-SI-Timestamp' => $timestamp,
			'X-SI-Nonce'     => $nonce,
			'X-SI-Signature' => hash_hmac( 'sha256', $message, $this->site_token ),
		);
	}

	/**
	 * Check whether an endpoint is safe for sending Cloud credentials.
	 *
	 * @param string $url Endpoint URL.
	 * @return bool
	 */
	private function is_safe_endpoint( $url ) {
		$scheme = strtolower( (string) wp_parse_url( (string) $url, PHP_URL_SCHEME ) );
		if ( 'https' === $scheme ) {
			return true;
		}

		if ( 'http' !== $scheme ) {
			return false;
		}

		$host = strtolower( (string) wp_parse_url( (string) $url, PHP_URL_HOST ) );

		return in_array( $host, array( 'localhost', '127.0.0.1', '::1', 'host.docker.internal' ), true );
	}

	/**
	 * Extract readable API error message.
	 *
	 * @param array<string,mixed> $data Response body.
	 * @param int                 $status HTTP status.
	 * @return string
	 */
	private function error_message( array $data, $status ) {
		if ( ! empty( $data['error'] ) && is_array( $data['error'] ) && ! empty( $data['error']['message'] ) ) {
			return sanitize_text_field( (string) $data['error']['message'] );
		}

		if ( ! empty( $data['message'] ) ) {
			return sanitize_text_field( (string) $data['message'] );
		}

		return sprintf(
			/* translators: %d: HTTP status code. */
			__( 'Cloud API returned HTTP %d.', 'submission-inbox' ),
			absint( $status )
		);
	}

	/**
	 * Extract API error code.
	 *
	 * @param array<string,mixed> $data Response body.
	 * @param string              $fallback Fallback code.
	 * @return string
	 */
	private function error_code( array $data, $fallback ) {
		if ( ! empty( $data['error'] ) && is_array( $data['error'] ) && ! empty( $data['error']['code'] ) ) {
			return sanitize_key( (string) $data['error']['code'] );
		}

		return sanitize_key( (string) $fallback );
	}

	/**
	 * Extract validation fields from API error envelope.
	 *
	 * @param array<string,mixed> $data Response body.
	 * @return array<string,mixed>
	 */
	private function error_fields( array $data ) {
		if ( ! empty( $data['error'] ) && is_array( $data['error'] ) && is_array( $data['error']['fields'] ?? null ) ) {
			return $data['error']['fields'];
		}

		return array();
	}
}
