<?php

namespace PostenBringCheckout\admin\bring;

use Exception;
use PostenBringCheckout\admin\api\models\MybringCustomerAndServices;
use PostenBringCheckout\admin\bring\models\address\Address;
use PostenBringCheckout\admin\bring\models\address\AddressSuggestions;
use PostenBringCheckout\admin\bring\models\booking\MyBringBookingResponse;
use PostenBringCheckout\admin\bring\models\booking\ValidationRequest;
use PostenBringCheckout\admin\bring\models\booking\ValidationResponse;
use PostenBringCheckout\admin\bring\models\booking\WebshopBooking;
use PostenBringCheckout\admin\api\models\CredentialsVerificationResult;
use PostenBringCheckout\admin\bring\models\notification\Notifications;
use PostenBringCheckout\admin\bring\models\service\MybringServiceDetails;

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

/**
 * All interactions between plugin and checkout api
 */
class CheckoutApi {

	/**
	 * Instance.
	 *
	 * @since 0.0.1
	 * @var CheckoutApi $instance Instance
	 */
	private static $instance;

	/**
	 * Constructor
	 *
	 * @since 0.0.1
	 */
	private function __construct() {}

	/**
	 * Get instance.
	 *
	 * @return CheckoutApi instance.
	 * @since 0.0.1
	 */
	public static function get_instance() {
		if ( ! isset( self::$instance ) ) {
			self::$instance = new CheckoutApi();
		}

		return self::$instance;
	}

	/**
	 * Verifies credentials
	 *
	 * @param $apiKey
	 * @param $webshopId
	 *
	 * @return CredentialsVerificationResult
	 * @throws Exception
	 */
	public function verifyCredentials($apiKey, $webshopId ): CredentialsVerificationResult {
		$url        = POSTEN_BRING_CHECKOUT_MYBRING_API_URL . "/checkout/webshops/$webshopId/verify";
		$request_id = $this->generate_request_id();

		$response = wp_remote_get($url, array(
			'headers' => array_merge(
				array(
					'X-MyBring-Token' => $apiKey,
					'X-Request-Id' => $request_id,
					'Accept' => 'application/json'
				),
				$this->getVersionHeaders()
			)
		));

		if ( is_wp_error( $response ) ) {
			$error_msg = $response->get_error_message();
			throw new Exception( esc_html("Could not validate credentials: $error_msg. Request Id: $request_id") );
		}

		$httpCode = wp_remote_retrieve_response_code( $response );

		switch ( $httpCode ) {
			case 200:
				return CredentialsVerificationResult::valid();
			case 401:
				return new CredentialsVerificationResult( false, true, true );
			case 403:
				return new CredentialsVerificationResult( true, true, false );
			case 404:
				return new CredentialsVerificationResult( true, false, true );
			default:
				throw new Exception( 'Could not validate credentials' );
		}
	}

	/**
	 * Installs the webshop in mybring
	 *
	 * @param $apiKey
	 * @param $webshopId
	 *
	 * @return void
	 * @throws Exception
	 */
	public function installWebshop($apiKey, $webshopId ): void {
		$url        = POSTEN_BRING_CHECKOUT_MYBRING_API_URL . "/checkout/webshops/$webshopId/install";
		$request_id = $this->generate_request_id();

		$response = wp_remote_request( $url, array(
			'method' => 'PUT',
			'headers' => array_merge(
				array(
					'X-MyBring-Token' => $apiKey,
					'X-Request-Id' => $request_id,
					'Accept' => 'application/json',
					'Content-Type' => 'application/json'
				),
				$this->getVersionHeaders()
			)
		) );

		if ( is_wp_error( $response ) ) {
			$error_msg = $response->get_error_message();
			throw new Exception( esc_html("Could not install shop:  $error_msg.  Request Id: $request_id") );
		}

		$httpCode = wp_remote_retrieve_response_code( $response );

		if ( $httpCode !== 200 ) {
			throw new Exception( esc_html("Failed to install webshop. HTTP Status Code: $httpCode. Request Id: $request_id") );
		}
	}

	/**
	 * Installs the webshop in mybring
	 *
	 * @param $apiKey
	 * @param $webshopId
	 *
	 * @return void
	 * @throws Exception
	 */
	public function uninstallWebshop($apiKey, $webshopId, $uninstallReason ): void {
		$base_url = defined('POSTEN_BRING_CHECKOUT_MYBRING_API_URL') ? POSTEN_BRING_CHECKOUT_MYBRING_API_URL : 'https://api.bring.com';
		$url        = $base_url . "/checkout/webshops/$webshopId/uninstall";
		$request_id = $this->generate_request_id();

		$response = wp_remote_request( $url, array(
			'method' => 'PUT',
			'headers' => array_merge(
				array(
					'X-MyBring-Token' => $apiKey,
					'X-Request-Id' => $request_id,
					'Accept' => 'application/json',
					'Content-Type' => 'application/json'
				),
				$this->getVersionHeaders()
			),
			'body' => wp_json_encode(
				is_array( $uninstallReason ) && empty( $uninstallReason )
				? (object) []
				: $uninstallReason
			)
		) );

		if ( is_wp_error( $response ) ) {
			$error_msg = $response->get_error_message();
			throw new Exception( esc_html('Could not uninstall shop: ' . $error_msg) );
		}

		$httpCode = wp_remote_retrieve_response_code( $response );

		if ( $httpCode !== 200 ) {
			throw new Exception( esc_html("Failed to uninstall webshop. HTTP Status Code: $httpCode. Request Id: $request_id") );
		}
	}

	/**
	 * Checks service availability
	 *
	 * @param string $from_country From country.
	 * @param string $from_postal_code From postal code.
	 * @param string $to_country To country.
	 * @param string $to_postal_code To postal code.
	 * @param int $total_weight Total weight.
	 * @param bool $exclude_mailbox Whether mailbox should be excluded or not.
	 * @param bool $exclude_lockers Whether lockers should be excluded or not.
	 *
	 * @return bool Whether shipping option is available or not
	 * @throws Exception Unable to check service availability
	 */
	public function checkServiceAvailability(
		string $from_country,
		string $from_postal_code,
		string $to_country,
		string $to_postal_code,
		int $total_weight,
		bool $exclude_mailbox,
		bool $exclude_lockers
	): bool {
		$body = wp_json_encode(
			array(
				'fromPostalCode' => $from_postal_code,
				'fromCountry'    => $from_country,
				'toPostalCode'   => $to_postal_code,
				'toCountry'      => $to_country,
				'totalWeight'    => $total_weight,
				'excludeMailbox' => $exclude_mailbox,
				'excludeLockers' => $exclude_lockers
			)
		);

		$transient_ref = 'posten_bring_checkout_' . md5( $body );
		$transient     = get_transient( $transient_ref );

		if ( $transient !== false ) {
			return $transient['available'];
		} else {
			$credentials = CredentialsService::get_instance()->get_credentials_and_webshop_id();

			if ( empty( $credentials->webshop_id ) || empty( $credentials->apikey ) ) {
				return false;
			}

			$url        = POSTEN_BRING_CHECKOUT_MYBRING_API_URL . "/checkout/shipping-rates/woo/$credentials->webshop_id/availability";
			$request_id = $this->generate_request_id();

			$response = wp_remote_post( $url, array(
				'headers' => array_merge(
					array(
						'X-MyBring-Token' => $credentials->apikey,
						'X-Request-Id' => $request_id,
						'Accept' => 'application/json',
						'Content-Type' => 'application/json'
					),
					$this->getVersionHeaders()
				),
				'body' => $body
			) );

			if ( is_wp_error( $response ) ) {
				$error_msg = $response->get_error_message();
				throw new Exception( esc_html("Could not check service availability: $error_msg. Request Id: $request_id") );
			}

			$httpCode = wp_remote_retrieve_response_code( $response );

			if ( $httpCode !== 200 ) {
				throw new Exception( esc_html("Failed to check service availability - got response: $httpCode. Request Id: $request_id") );
			}
			$responseData = json_decode( wp_remote_retrieve_body( $response ), true );
			set_transient( $transient_ref, $responseData, 60 );

			return $responseData['available'];
		}
	}

	public function requires_hs_code(string $from_postal_code, string $to_postal_code ): bool {
		$credentials = CredentialsService::get_instance()->get_credentials_and_webshop_id();

		$transient_ref = 'posten_bring_checkout_nvit_' . md5( $from_postal_code . $to_postal_code );
		$transient     = get_transient( $transient_ref );

		if ( $transient !== false ) {
			return $transient['requiresHsCode'] ?? false;
		} else {
			$url        = POSTEN_BRING_CHECKOUT_MYBRING_API_URL . "/checkout/customs/nvit?fromPostalCode=$from_postal_code&toPostalCode=$to_postal_code";
			$request_id = $this->generate_request_id();

			$response = wp_remote_get( $url, array(
				'headers' => array_merge(
					array(
						'X-MyBring-Token' => $credentials->apikey,
						'X-Request-Id' => $request_id,
						'Accept' => 'application/json',
					),
					$this->getVersionHeaders()
				)
			));

			if ( is_wp_error( $response ) ) {
				$error_msg = esc_html($response->get_error_message());
				wc_get_logger()->warning( "Could not check NVIT: $error_msg. Request Id: $request_id", array( 'source' => 'posten-bring-checkout', 'remote_log' => false ) );
				return false;
			}

			$httpCode = wp_remote_retrieve_response_code( $response );

			if ( $httpCode !== 200 ) {
				wc_get_logger()->warning( "Could not check NVIT. Request Id: $request_id", array( 'source' => 'posten-bring-checkout', 'remote_log' => false ) );
				return false;
			} else {
				$responseData = json_decode( wp_remote_retrieve_body( $response ), true );
				set_transient( $transient_ref, $responseData, 480 );

				return $responseData['requiresHsCode'] ?? false;
			}
		}
	}

	public function query_hs_code( string $query ): array {
		$credentials = CredentialsService::get_instance()->get_credentials_and_webshop_id();

		$current_locale    = get_locale();
		if ( function_exists( 'icl_get_languages' ) ) { // If WPML is installed.
			$current_locale = apply_filters( 'wpml_current_language', NULL );
		} elseif ( function_exists( 'pll_current_language' ) ) { // If Polylang is installed.
			$current_locale = pll_current_language();
		}

		$transient_ref = 'posten_bring_checkout_hs_code_' . md5( $current_locale . $query );
		$transient     = get_transient( $transient_ref );

		if ( $transient !== false ) {
			return $transient;
		} else {
			$url        = add_query_arg( 'q', $query, POSTEN_BRING_CHECKOUT_MYBRING_API_URL . '/checkout/customs/hscode');
			$request_id = $this->generate_request_id();

			$response = wp_remote_get( $url, array(
				'headers' => array_merge(
					array(
						'X-MyBring-Token' => $credentials->apikey,
						'Accept-Language' => $current_locale,
						'X-Request-Id' => $request_id,
						'Accept' => 'application/json',
					),
					$this->getVersionHeaders()
				)
			));

			if ( is_wp_error( $response ) ) {
				$error_msg = esc_html($response->get_error_message());
				wc_get_logger()->warning( "Could not get HS codes: $error_msg. Request Id: $request_id", array( 'source' => 'posten-bring-checkout', 'remote_log' => false ) );
				return array();
			}

			$httpCode = wp_remote_retrieve_response_code( $response );

			if ( $httpCode !== 200 ) {
				wc_get_logger()->warning( "Could not get HS codes. Request Id: $request_id", array( 'source' => 'posten-bring-checkout', 'remote_log' => false ) );
				return array();
			} else {
				$responseData = json_decode( wp_remote_retrieve_body( $response ), true );
				set_transient( $transient_ref, $responseData, 60 );

				return $responseData;
			}
		}
	}

	public function getShippingRates(
		string $from_country,
		string $from_postal_code,
		string $to_country,
		string $to_postal_code,
		?string $address,
		string $total_cost,
		array $items,
		string $locale,
		string $currency,
		bool $exclude_lockers,
		bool $exclude_mailbox,
		bool $free_shipping_from_coupon,
		bool $include_groupage_pallet
	) {
		$body = wp_json_encode(
			array(
				'fromPostalCode'       => $from_postal_code,
				'fromCountry'          => $from_country,
				'toPostalCode'         => $to_postal_code,
				'toCountry'            => $to_country,
				'address1'             => $address,
				'totalCost'            => $total_cost,
				'items'                => $items,
				'currency'             => $currency,
				'locale'               => $locale,
				'excludeLockers'       => $exclude_lockers,
				'excludeMailbox'       => $exclude_mailbox,
				'numberOfPickupPoints' => 10,
				'includeGroupageAndPallet' => $include_groupage_pallet,
				'freeShippingFromCoupon' => $free_shipping_from_coupon
			)
		);

		$transient_ref = 'posten_bring_checkout_' . md5( $body );
		$transient     = get_transient( $transient_ref );

		if ( $transient !== false ) {
			return $transient;
		} else {
			$credentials = CredentialsService::get_instance()->get_credentials_and_webshop_id();

			$url        = POSTEN_BRING_CHECKOUT_MYBRING_API_URL . "/checkout/shipping-rates/woo/$credentials->webshop_id/rates";
			$request_id = $this->generate_request_id();

			$response = wp_remote_post( $url, array(
				'headers' => array_merge(
					array(
						'X-MyBring-Token' => $credentials->apikey,
						'X-Request-Id' => $request_id,
						'Accept' => 'application/json',
						'Content-Type' => 'application/json'
					),
					$this->getVersionHeaders()
				),
				'body' => $body
			) );

			if ( is_wp_error( $response ) ) {
				$error_msg = $response->get_error_message();
				throw new Exception( esc_html("Could not get shipping option rates: $error_msg. Request Id: $request_id") );
			}

			$httpCode = wp_remote_retrieve_response_code( $response );

			if ( $httpCode !== 200 ) {
				throw new Exception( esc_html("Failed to get shipping option rates - got response: $httpCode. Request Id: $request_id") );
			}
			$responseData = json_decode( wp_remote_retrieve_body( $response ), true );
			set_transient( $transient_ref, $responseData, 60 );

			return $responseData;
		}
	}

	/**
	 * @param WebshopBooking $bookingRequest
	 *
	 * @return array
	 * @throws Exception
	 */
	public function createBooking(
		WebshopBooking $bookingRequest
	): array {
		$credentials = CredentialsService::get_instance()->get_credentials_and_webshop_id();

		$url        = POSTEN_BRING_CHECKOUT_MYBRING_API_URL . "/checkout/webshops/{$credentials->webshop_id}/bookings";
		$request_id = $this->generate_request_id();
		$headers    = array_merge(
			array(
				"X-MyBring-Token: {$credentials->apikey}",
				"X-Request-Id: $request_id",
				'Accept: application/json',
				'Content-Type: application/json',
			),
			$this->getVersionHeaders()
		);

		try {
			$body = wp_json_encode( $bookingRequest->toArray() );
		} catch ( Exception $e ) {
			wc_get_logger()->error(
				"Could not construct booking request for order $bookingRequest->externalOrderId. - " . esc_html($e->getMessage()),
				array(
					'source'    => 'posten-bring-checkout',
					'backtrace' => true,
				)
			);
		}

		$response = wp_remote_post( $url, array(
			'headers' => array_merge(
				array(
					'X-MyBring-Token' => $credentials->apikey,
					'X-Request-Id' => $request_id,
					'Accept' => 'application/json',
					'Content-Type' => 'application/json'
				),
				$this->getVersionHeaders()
			),
			'body' => $body
		) );

		if ( is_wp_error( $response ) ) {
			$error_msg = $response->get_error_message();
			throw new Exception( esc_html("Could not create booking for order $bookingRequest->externalOrderId: $error_msg. Request Id: $request_id") );
		}

		$httpCode = wp_remote_retrieve_response_code( $response );

		$responseData = json_decode( wp_remote_retrieve_body( $response ), true );

		if ( $httpCode !== 200 ) {
			if ( isset( $responseData['code'] ) && $responseData['code'] === 'INVALID_ORIGIN_ADDRESS' ) {
				throw new Exception( esc_html(__( 'The postal code of the origin location is invalid.', 'posten-bring-checkout' )) );
			} else {
				wc_get_logger()->error(
					"An error occurred while booking shipment for order $bookingRequest->externalOrderId. Request Id:  $request_id",
					array(
						'source'    => 'posten-bring-checkout',
						'backtrace' => true,
					)
				);
				throw new Exception( esc_html(__( 'An error occurred while booking the shipment. ', 'posten-bring-checkout' )));
			}
		}

		$bookings = array();
		foreach ( $responseData as $data ) {
			$bookings[] = new MyBringBookingResponse( $data );
		}

		return $bookings;
	}

	public function get_postal_code_details( string $country, string $postal_code ): array {
		$url = "https://api.bring.com/address/api/open/postalCode/postalCode.json?pnr={$postal_code}&country={$country}";

		$response = wp_remote_get( $url, array(
			'headers' => array(
				'Accept' => 'application/json'
			)
		) );

		if ( is_wp_error( $response ) ) {
			$error_msg = $response->get_error_message();
			throw new Exception( esc_html('Could not validate postal code: ' . $error_msg) );
		}

		$httpCode = wp_remote_retrieve_response_code( $response );

		if ( $httpCode !== 200 ) {
			throw new Exception( esc_html("Failed to create booking - got response: $httpCode, body: " . $response) );
		}

		$responseData = json_decode( wp_remote_retrieve_body( $response ), true );

		return [
			'isValid' => $responseData['valid'] ?? false,
			'isPoBox' => isset($responseData['postalCodeType']) && $responseData['postalCodeType'] === 'POBOX'
		];
	}

	public function get_shipping_options( ?string $from, ?string $to ): array {
		$credentials = CredentialsService::get_instance()->get_credentials_and_webshop_id();

		$url        = POSTEN_BRING_CHECKOUT_MYBRING_API_URL . "/checkout/webshops/$credentials->webshop_id/services" . ( $from && $to ? "?originCountryCode={$from}&destinationCountryCode={$to}" : '' );
		$request_id = $this->generate_request_id();

		$response = wp_remote_get( $url, array(
			'headers' => array_merge(
				array(
					'X-MyBring-Token' => $credentials->apikey,
					'X-Request-Id' => $request_id,
					'Accept' => 'application/json',
					'Content-Type' => 'application/json'
				),
				$this->getVersionHeaders()
			),
		) );

		if ( is_wp_error( $response ) ) {
			$error_msg = $response->get_error_message();
			throw new Exception( esc_html("Could not get services:  $error_msg. Request Id: $request_id") );
		}

		$http_code = wp_remote_retrieve_response_code( $response );

		if ( $http_code !== 200 ) {
			throw new Exception( esc_html("Failed to get services - got response: $http_code. Request Id: $request_id") );
		}

		$response_data = json_decode( wp_remote_retrieve_body( $response ), true );

		return array_map(
			function ( $service_details ) {
				return new MybringServiceDetails( $service_details );
			},
			$response_data
		);
	}


	/**
	 * @param string $from
	 * @param string $to
	 *
	 * @return array
	 * @throws Exception
	 */
	public function getBookingCustomersAndServices( string $from, string $to ): array {
		$credentials = CredentialsService::get_instance()->get_credentials_and_webshop_id();

		$url        = POSTEN_BRING_CHECKOUT_MYBRING_API_URL . "/checkout/webshops/{$credentials->webshop_id}/bookings/services-customers?fromCountry={$from}&toCountry={$to}";
		$request_id = $this->generate_request_id();

		$response = wp_remote_get( $url, array(
			'headers' => array_merge(
				array(
					'X-MyBring-Token' => $credentials->apikey,
					'X-Request-Id' => $request_id,
					'Accept' => 'application/json',
					'Content-Type' => 'application/json'
				),
				$this->getVersionHeaders()
			),
		) );

		if ( is_wp_error( $response ) ) {
			$error_msg = $response->get_error_message();
			throw new Exception( esc_html("Could not get booking customers and services: $error_msg. Request Id: $request_id") );
		}

		$httpCode = wp_remote_retrieve_response_code( $response );

		if ( $httpCode !== 200 ) {
			throw new Exception( esc_html("Failed to get booking customers and services - got response: $httpCode. Request Id: $request_id") );
		}

		$responseData = json_decode( wp_remote_retrieve_body( $response ), true );

		$customersAndServices = array();
		foreach ( $responseData as $data ) {
			$customersAndServices[] = new MybringCustomerAndServices( $data );
		}

		return $customersAndServices;
	}

	public function validateOrders( ValidationRequest $validation_request ): ValidationResponse {
		try {
			$credentials = CredentialsService::get_instance()->get_credentials_and_webshop_id();

			$current_locale    = get_locale();
			if ( function_exists( 'icl_get_languages' ) ) { // If WPML is installed.
				$current_locale = apply_filters( 'wpml_current_language', NULL );
			} elseif ( function_exists( 'pll_current_language' ) ) { // If Polylang is installed.
				$current_locale = pll_current_language();
			}

			$url        = POSTEN_BRING_CHECKOUT_MYBRING_API_URL . "/checkout/webshops/{$credentials->webshop_id}/bookings/validation";
			$request_id = $this->generate_request_id();

			$response = wp_remote_post( $url, array(
				'headers' => array_merge(
					array(
						'X-MyBring-Token' => $credentials->apikey,
						'X-Request-Id' => $request_id,
						'Accept' => 'application/json',
						'Content-Type' => 'application/json',
						'Accept-language' => $current_locale
					),
					$this->getVersionHeaders()
				),
				'body' => wp_json_encode( $validation_request->toRequestObject() )
			) );

			if ( is_wp_error( $response ) ) {
				$error_msg = $response->get_error_message();
				throw new Exception( esc_html("Could not retrieve bookings: $error_msg. Request Id: $request_id") );
			}

			$httpCode = wp_remote_retrieve_response_code( $response );

			if ( $httpCode !== 200 ) {
				throw new Exception( esc_html("Failed to retrieve bookings - got response: $httpCode, request id: $request_id, body: " . wp_remote_retrieve_body( $response ) ) );
			}

			$responseData = json_decode( wp_remote_retrieve_body( $response ), true );

			return new ValidationResponse( $responseData );
		} catch ( Exception $exception ) {
			wc_get_logger()->warning( esc_html($exception->getMessage()), array( 'source' => 'posten-bring-checkout' ) );

			return new ValidationResponse( array() );
		}
	}


	public function getLabels( string $orderIds, string $labelType ): string {
		$credentials = CredentialsService::get_instance()->get_credentials_and_webshop_id();

		$url        = POSTEN_BRING_CHECKOUT_MYBRING_API_URL . "/checkout/webshops/{$credentials->webshop_id}/labels?orderIds={$orderIds}&labelType={$labelType}";
		$request_id = $this->generate_request_id();

		if (ob_get_length()) {
			ob_end_clean();
		}

		$response = wp_remote_get( $url, array(
			'headers' => array_merge(
				array(
					'X-MyBring-Token' => $credentials->apikey,
					'X-Request-Id' => $request_id,
				),
				$this->getVersionHeaders()
			),
			'decompress' => false,
		) );

		if ( is_wp_error( $response ) ) {
			$error_msg = $response->get_error_message();
			throw new Exception( esc_html("Could not retrieve labels: $error_msg. Request Id: $request_id") );
		}

		$httpCode = wp_remote_retrieve_response_code( $response );

		if ( $httpCode !== 200 ) {
			throw new Exception( esc_html("Failed to retrieve labels - got response: $httpCode, request id: $request_id, body: " . wp_remote_retrieve_body( $response ) ) );
		}

		return wp_remote_retrieve_body($response);
	}

	public function getWaybills( string $orderIds ): string {
		$credentials = CredentialsService::get_instance()->get_credentials_and_webshop_id();

		$url        = POSTEN_BRING_CHECKOUT_MYBRING_API_URL . "/checkout/webshops/{$credentials->webshop_id}/waybills?orderIds={$orderIds}";
		$request_id = $this->generate_request_id();

		if (ob_get_length()) {
			ob_end_clean();
		}

		$response = wp_remote_get( $url, array(
			'headers' => array_merge(
				array(
					'X-MyBring-Token' => $credentials->apikey,
					'X-Request-Id' => $request_id,
				),
				$this->getVersionHeaders()
			),
			'decompress' => false,
		) );

		if ( is_wp_error( $response ) ) {
			$error_msg = $response->get_error_message();
			throw new Exception( esc_html("Could not retrieve waybills: $error_msg. Request Id: $request_id") );
		}

		$httpCode = wp_remote_retrieve_response_code( $response );

		if ( $httpCode !== 200 ) {
			throw new Exception( esc_html("Failed to retrieve waybills - got response: $httpCode, request id: $request_id, body: " . wp_remote_retrieve_body( $response ) ) );
		}

		return wp_remote_retrieve_body($response);
	}

	/**
	 * Fetches operational issues and general announcements from Bring
	 *
	 * @return void
	 */
	public function get_notifications(): Notifications {
		try {
			$credentials = CredentialsService::get_instance()->get_credentials_and_webshop_id();
			if ( empty( $credentials->apikey ) || empty( $credentials->webshop_id ) ) {
				return new Notifications( array(), array() );
			}

			$current_locale    = get_locale();
			if ( function_exists( 'icl_get_languages' ) ) { // If WPML is installed.
				$current_locale = apply_filters( 'wpml_current_language', NULL );
			} elseif ( function_exists( 'pll_current_language' ) ) { // If Polylang is installed.
				$current_locale = pll_current_language();
			}

			$transient_ref = 'posten_bring_checkout_notifications' . $current_locale;
			$transient     = get_transient( $transient_ref );

			if ( $transient !== false ) {
				return new Notifications( $transient['errors'], $transient['news'] );
			}

			$url        = POSTEN_BRING_CHECKOUT_MYBRING_API_URL . "/checkout/webshops/{$credentials->webshop_id}/notifications";
			$request_id = $this->generate_request_id();

			$response = wp_remote_get( $url, array(
				'headers' => array_merge(
					array(
						'X-MyBring-Token' => $credentials->apikey,
						'X-Request-Id' => $request_id,
						'Accept' => 'application/json',
						'Accept-language' => explode('_', $current_locale)[0]
					),
					$this->getVersionHeaders()
				)
			) );

			if ( is_wp_error( $response ) ) {
				$error_msg = $response->get_error_message();
				throw new Exception( esc_html("Could not retrieve notifications: $error_msg. Request Id: $request_id") );
			}

			$httpCode = wp_remote_retrieve_response_code( $response );

			if ( $httpCode !== 200 ) {
				throw new Exception( esc_html("Failed to retrieve notifications - got response: $httpCode, request id: $request_id, body: " . wp_remote_retrieve_body( $response ) ) );
			}

			$responseData = json_decode( wp_remote_retrieve_body( $response ), true );
			set_transient( $transient_ref, $responseData, 60 * 5);

			return new Notifications( $responseData['errors'], $responseData['news'] );

		} catch ( Exception $exception ) {
			wc_get_logger()->warning( esc_html($exception->getMessage()), array( 'source' => 'posten-bring-checkout' ) );
			return new Notifications( array(), array() );
		}
	}

	/**
	 * Sends feedback to the Bring API
	 *
	 * @param string $category The category of the feedback
	 * @param string $message The feedback message.
	 * @param string $email The email address of the person providing the feedback.
	 *
	 * @return bool Whether the feedback submission was successful or not.
	 * @throws Exception Unable to send feedback.
	 */
	public function sendFeedback(
		string $category,
		string $message,
		string $email
	): bool {
		$body = wp_json_encode(
			array(
				'category' => $category,
				'message'  => $message,
				'email'    => $email,
			)
		);

		$credentials = CredentialsService::get_instance()->get_credentials_and_webshop_id();

		$url        = POSTEN_BRING_CHECKOUT_MYBRING_API_URL . "/checkout/webshops/{$credentials->webshop_id}/feedback";
		$request_id = $this->generate_request_id();

		$response = wp_remote_post( $url, array(
			'headers' => array_merge(
				array(
					'X-MyBring-Token' => $credentials->apikey,
					'X-Request-Id' => $request_id,
					'Accept' => 'application/json',
					'Content-Type' => 'application/json'
				),
				$this->getVersionHeaders()
			),
			'body' => $body
		) );

		if ( is_wp_error( $response ) ) {
			$error_msg = $response->get_error_message();
			throw new Exception( esc_html("Could not send feedback: $error_msg. Request Id: $request_id") );
		}

		$httpCode = wp_remote_retrieve_response_code( $response );

		return $httpCode === 200;
	}

	/**
	 * Log
	 *
	 * @param string $message The log message.
	 * @param string $level The log level.
	 * @param string $stack The detailed error stack - if applicable.
	 *
	 * @return bool Whether the log entry submission was successful or not.
	 */
	public function remote_log(string $message, string $level, $stack): bool {
		try {
			$log_request = array(
				'message' => $message,
				'level' => $level
			);

			if ( !empty($stack) ) {
				$log_request['stack'] = $stack;
			}

			$body = wp_json_encode( $log_request );

			$credentials = CredentialsService::get_instance()->get_credentials_and_webshop_id();

			if ( !empty( $credentials->apikey ) && !empty( $credentials->webshop_id ) ) {
				$url        = POSTEN_BRING_CHECKOUT_MYBRING_API_URL . "/checkout/webshops/{$credentials->webshop_id}/log";
				$request_id = $this->generate_request_id();

				$response = wp_remote_post( $url, array(
					'headers' => array_merge(
						array(
							'X-MyBring-Token' => $credentials->apikey,
							'X-Request-Id' => $request_id,
							'Accept' => 'application/json',
							'Content-Type' => 'application/json'
						),
						$this->getVersionHeaders()
					),
					'body' => $body
				) );

				if ( is_wp_error( $response ) ) {
					$error_msg = esc_html($response->get_error_message());
					wc_get_logger()->warning( "Could not submit remote log entry: $error_msg. Request Id: $request_id", array( 'source' => 'posten-bring-checkout', 'remote_log' => false ) );
				}

				$http_code = wp_remote_retrieve_response_code( $response );

				if ( $http_code !== 200 ) {
					wc_get_logger()->warning( "Could not submit remote log entry. Request Id: $request_id", array( 'source' => 'posten-bring-checkout', 'remote_log' => false ) );
				}

				return $http_code === 200;
			} else {
				wc_get_logger()->warning( "Could not submit remote log entry. Posten Bring Checkout is not activated.", array( 'source' => 'posten-bring-checkout', 'remote_log' => false ) );
				return false;
			}

		} catch ( Exception $exception ) {
			wc_get_logger()->warning( esc_html($exception->getMessage()), array( 'source' => 'posten-bring-checkout', 'remote_log' => false ) );
			return false;
		}
	}

	/**
	 * Get address suggestions
	 *
	 * @param string $query The address query.
	 * @param string $countryCode The selected country code.
	 *
	 * @return AddressSuggestions Address suggestions matching provided query.
	 * @throws Exception
	 */
	public function get_address_suggestions(string $query, string $countryCode): AddressSuggestions {
		$credentials = CredentialsService::get_instance()->get_credentials_and_webshop_id();

		$url        = POSTEN_BRING_CHECKOUT_MYBRING_API_URL . "/checkout/address?q={$query}&countryCode={$countryCode}";
		$request_id = $this->generate_request_id();

		$response = wp_remote_get( $url, array(
			'headers' => array_merge(
				array(
					'X-MyBring-Token' => $credentials->apikey,
					'X-Request-Id' => $request_id,
					'Accept' => 'application/json'
				),
				$this->getVersionHeaders()
			)
		) );

		if ( is_wp_error( $response ) ) {
			$error_msg = $response->get_error_message();
			throw new Exception( "Could not fetch address suggestions: $error_msg. Request Id: $request_id" );
		}

		$http_code = wp_remote_retrieve_response_code( $response );

		if ( $http_code !== 200 ) {
			throw new Exception( "Failed to fetch address suggestions - got response: $http_code, request id: $request_id, body: " . wp_remote_retrieve_body( $response ) );
		}

		$responseData = json_decode( wp_remote_retrieve_body( $response ), true );

		if ( !is_array( $responseData )|| !array_key_exists( 'suggestions', $responseData ) || !is_array( $responseData['suggestions'] ) ) {
			return new AddressSuggestions( array() );
		}

		return new AddressSuggestions( $responseData['suggestions'] );

	}

	/**
	 * Get address details
	 *
	 * @param string $addressId The address id to fetch details for.
	 *
	 * @return Address Address details.
	 * @throws Exception
	 */
	public function get_address(string $addressId): Address {
		$credentials = CredentialsService::get_instance()->get_credentials_and_webshop_id();

		$url        = POSTEN_BRING_CHECKOUT_MYBRING_API_URL . "/checkout/address/{$addressId}";
		$request_id = $this->generate_request_id();

		$response = wp_remote_get( $url, array(
			'headers' => array_merge(
				array(
					'X-MyBring-Token' => $credentials->apikey,
					'X-Request-Id' => $request_id,
					'Accept' => 'application/json'
				),
				$this->getVersionHeaders()
			)
		) );

		if ( is_wp_error( $response ) ) {
			$error_msg = $response->get_error_message();
			throw new Exception("Could not fetch address details: $error_msg. Request Id: $request_id");
		}

		$http_code = wp_remote_retrieve_response_code( $response );

		if ( $http_code !== 200 ) {
			throw new Exception("Failed to fetch address details - got response: $http_code, request id: $request_id, body: " . wp_remote_retrieve_body( $response ));
		}

		$responseData = json_decode( wp_remote_retrieve_body( $response ), true );

		if ( !is_array( $responseData ) ) {
			throw new Exception( "No address details received for address id: $addressId" );
		}

		return new Address( $responseData['id'], $responseData['address'], $responseData['city'], $responseData['postalCode'], $responseData['countryCode'] );
	}


	private function getVersionHeaders(): array {
		global $wp_version;
		$woo_express = get_option( 'wooexpress_navigation_enabled' );

		if ( is_plugin_active( 'woo-vipps/woo-vipps.php' ) ) {
			$vipps_details = get_file_data( WP_PLUGIN_DIR . '/woo-vipps/woo-vipps.php', array( 'Version' => 'Version' ), false );
		}

		if ( is_plugin_active( 'dintero-checkout-for-woocommerce/dintero-checkout-for-woocommerce.php' ) ) {
			$dintero_details = get_file_data( WP_PLUGIN_DIR . '/dintero-checkout-for-woocommerce/dintero-checkout-for-woocommerce.php', array( 'Version' => 'Version' ), false );
		}

		return array_merge(
			array(
				'X-WC-Version' => ( $woo_express ? 'WooExpress-' . WC()->version : WC()->version ),
				'X-WP-Version' => $wp_version,
				'X-WP-Theme' => wp_get_theme()->get( 'Name' ),
				'X-Woo-Plugin-Version' => POSTEN_BRING_CHECKOUT_VERSION,
				'X-Shop-Domain' => wp_parse_url( site_url() )['host'],
				'X-Woo-Checkout-Type' => $this->get_checkout_type()
			),
			!empty( $vipps_details['Version'] ) ? array(
				'X-Vipps-Version' => $vipps_details['Version']
			) : [],
			!empty( $dintero_details['Version'] ) ? array(
				'X-Dintero-Version' => $dintero_details['Version']
			) : [],
		);
	}

	private function get_checkout_type(): string {
		$transient_ref = 'posten_bring_checkout_shop_checkout_type';
		$checkout_type = get_transient( $transient_ref );

		if ( $checkout_type !== false ) {
			return $checkout_type;
		}

		if ( has_block( 'woocommerce/checkout', get_post_field( 'post_content', wc_get_page_id( 'checkout' ) ) ) ) {
			$checkout_type = 'block';
		} else {
			$checkout_type = 'shortcode';
		}

		set_transient( $transient_ref, $checkout_type, 3600 );

		return $checkout_type;
	}

	private function generate_request_id(): string {
		$characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';

		return substr( str_shuffle( str_repeat( $characters, ceil( 6 / strlen( $characters ) ) ) ), 0, 6 );
	}
}
