<?php
/**
 * Square API wrapper.
 *
 * @package SHC4WC
 */

// phpcs:disable Squiz.Commenting
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

class SHC4WC_API {
	private $settings;

	public function __construct( $settings ) {
		$this->settings = is_array( $settings ) ? $settings : array();
	}

	public function get_environment() {
		return ( isset( $this->settings['environment'] ) && 'live' === $this->settings['environment'] ) ? 'live' : 'sandbox';
	}

	public function get_api_base() {
		return ( 'live' === $this->get_environment() ) ? 'https://connect.squareup.com' : 'https://connect.squareupsandbox.com';
	}

	public function get_access_token( $force_refresh = false ) {
		return SHC4WC_OAuth_Token::get_access_token( $force_refresh );
	}

	public function log( $message, $context = array(), $level = 'info' ) {
		if ( empty( $this->settings['debug'] ) || 'yes' !== $this->settings['debug'] ) {
			return;
		}
		$line = $message . ' ' . wp_json_encode( $context );
		if ( function_exists( 'wc_get_logger' ) ) {
			wc_get_logger()->log( $level, $line, array( 'source' => 'shc4wc' ) );
			return;
		}
		// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
		error_log( '[shc4wc] ' . $line );
	}

	/**
	 * Normalize a buyer email for Square — fix common typos so checkout is not blocked.
	 *
	 * @param string $email Raw email.
	 * @return string Square-safe email, or empty when none can be derived.
	 */
	public static function normalize_email_for_square( $email ) {
		$email = strtolower( trim( sanitize_email( (string) $email ) ) );
		if ( '' === $email ) {
			return '';
		}

		$candidates = array( $email );
		$at         = strrpos( $email, '@' );
		if ( false !== $at ) {
			$local = substr( $email, 0, $at );
			$host  = substr( $email, $at + 1 );
			foreach ( self::fix_email_host_typos( $host ) as $fixed_host ) {
				$candidates[] = $local . '@' . $fixed_host;
			}
		}

		$candidates = array_unique( $candidates );
		foreach ( $candidates as $candidate ) {
			if ( self::is_acceptable_square_email( $candidate ) ) {
				return $candidate;
			}
		}

		return '';
	}

	/**
	 * @param string $host Email hostname (after @).
	 * @return string[]
	 */
	private static function fix_email_host_typos( $host ) {
		$host  = strtolower( sanitize_text_field( (string) $host ) );
		$fixes = array();

		$tld_fixes = array(
			'/.comk$/' => '.com',
			'/.comm$/' => '.com',
			'/.con$/'  => '.com',
			'/.cmo$/'   => '.com',
			'/.coom$/' => '.com',
			'/.nett$/' => '.net',
			'/.orgg$/' => '.org',
		);
		foreach ( $tld_fixes as $pattern => $replacement ) {
			$fixed = preg_replace( $pattern, $replacement, $host );
			if ( is_string( $fixed ) && $fixed !== $host && '' !== $fixed ) {
				$fixes[] = $fixed;
			}
		}

		$domain_fixes = array(
			'gmial.com'   => 'gmail.com',
			'gamil.com'   => 'gmail.com',
			'gmal.com'    => 'gmail.com',
			'gnail.com'   => 'gmail.com',
			'yaho.com'    => 'yahoo.com',
			'yahooo.com'  => 'yahoo.com',
			'hotmal.com'  => 'hotmail.com',
			'hotmial.com' => 'hotmail.com',
			'iclod.com'   => 'icloud.com',
		);
		if ( isset( $domain_fixes[ $host ] ) ) {
			$fixes[] = $domain_fixes[ $host ];
		}

		return array_values( array_unique( $fixes ) );
	}

	/**
	 * Whether Square is likely to accept this email (DNS + basic format).
	 *
	 * @param string $email Email address.
	 * @return bool
	 */
	private static function is_acceptable_square_email( $email ) {
		if ( ! is_email( $email ) || false === filter_var( $email, FILTER_VALIDATE_EMAIL ) ) {
			return false;
		}

		$host = strtolower( (string) substr( strrchr( $email, '@' ), 1 ) );
		if ( '' === $host || false === strpos( $host, '.' ) ) {
			return false;
		}

		if ( ! function_exists( 'checkdnsrr' ) ) {
			return true;
		}

		return checkdnsrr( $host, 'MX' ) || checkdnsrr( $host, 'A' ) || checkdnsrr( $host, 'AAAA' );
	}

	public function request( $method, $path, $body = null, $extra_headers = array(), $allow_auth_retry = true ) {
		$url     = trailingslashit( $this->get_api_base() ) . ltrim( $path, '/' );
		$headers = array_merge(
			$this->get_square_auth_headers(),
			$extra_headers
		);

		$args = array(
			'method'  => strtoupper( $method ),
			'headers' => $headers,
			'timeout' => 60,
		);

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

		$this->log(
			'Square API request',
			array(
				'url'    => $url,
				'method' => $method,
				'body'   => $body,
			)
		);
		$response = wp_remote_request( $url, $args );

		if ( is_wp_error( $response ) ) {
			$this->log( 'Square API wp_error', array( 'error' => $response->get_error_message() ), 'error' );
			return $response;
		}

		$code = (int) wp_remote_retrieve_response_code( $response );
		$raw  = wp_remote_retrieve_body( $response );
		$data = json_decode( $raw, true );
		if ( ! is_array( $data ) ) {
			$data = array();
		}

		$this->log(
			'Square API response',
			array(
				'code' => $code,
				'body' => $data,
			)
		);

		if ( 401 === $code && $allow_auth_retry && $this->should_retry_after_auth_failure( $data ) ) {
			$refreshed = SHC4WC_OAuth_Token::refresh_access_token( true );
			if ( ! is_wp_error( $refreshed ) && '' !== $refreshed ) {
				return $this->request( $method, $path, $body, $extra_headers, false );
			}
		}

		if ( $code < 200 || $code >= 300 ) {
			return new WP_Error(
				'shc4wc_square_api_error',
				'Square API request failed',
				array(
					'status' => $code,
					'data'   => $data,
				)
			);
		}

		return $data;
	}

	/**
	 * Authorization headers for Square Connect API calls.
	 *
	 * @return array<string, string>
	 */
	private function get_square_auth_headers() {
		return array(
			'Authorization'    => 'Bearer ' . $this->get_access_token(),
			'Content-Type'   => 'application/json',
			'Square-Version' => '2024-12-18',
		);
	}

	/**
	 * @param array<string, mixed> $data Square error payload.
	 */
	private function should_retry_after_auth_failure( array $data ) {
		if ( empty( SHC4WC_Plugin::get_oauth_data()['refresh_token'] ) ) {
			return false;
		}

		$errors = isset( $data['errors'] ) && is_array( $data['errors'] ) ? $data['errors'] : array();
		foreach ( $errors as $error ) {
			if ( ! is_array( $error ) ) {
				continue;
			}
			$code = isset( $error['code'] ) ? strtoupper( (string) $error['code'] ) : '';
			if ( in_array( $code, array( 'UNAUTHORIZED', 'ACCESS_TOKEN_EXPIRED', 'ACCESS_TOKEN_REVOKED' ), true ) ) {
				return 'ACCESS_TOKEN_REVOKED' !== $code;
			}
		}

		return true;
	}

	public function list_locations( $access_token = '' ) {
		$token = $access_token ? $access_token : $this->get_access_token();
		if ( empty( $token ) ) {
			return new WP_Error( 'shc4wc_no_access_token', 'Square access token missing.' );
		}

		$url = trailingslashit( $this->get_api_base() ) . 'v2/locations';
		$this->log( 'Square locations request', array( 'url' => $url ) );

		$response = wp_remote_get(
			$url,
			array(
				'headers' => array(
					'Authorization'  => 'Bearer ' . $token,
					'Content-Type'   => 'application/json',
					'Square-Version' => '2024-12-18',
				),
				'timeout' => 60,
			)
		);

		if ( is_wp_error( $response ) ) {
			$this->log( 'Square locations wp_error', array( 'error' => $response->get_error_message() ), 'error' );
			return $response;
		}

		$code = (int) wp_remote_retrieve_response_code( $response );
		$raw  = wp_remote_retrieve_body( $response );
		$data = json_decode( $raw, true );
		if ( ! is_array( $data ) ) {
			$data = array();
		}

		if ( $code < 200 || $code >= 300 ) {
			$this->log(
				'Square locations failed',
				array(
					'status' => $code,
					'body'   => $data,
				),
				'error'
			);
			return new WP_Error(
				'shc4wc_square_locations_error',
				'Square locations request failed',
				array(
					'status' => $code,
					'data'   => $data,
				)
			);
		}

		return $data;
	}

	/**
	 * Create a Square Hosted Checkout payment link.
	 *
	 * @param float  $amount      Major currency units (e.g. 25.00).
	 * @param string $currency    ISO currency code.
	 * @param string $reference   Unique reference id stored on the Square order.
	 * @param array  $args        Optional description, return_args, line_name, line_items, location_id,
	 *                            ask_for_shipping_address, buyer_address, shipping_fee.
	 * @return array|WP_Error
	 */
	public function create_payment_link( $amount, $currency, $reference, $args = array() ) {
		$settings    = $this->settings;
		$location_id = isset( $args['location_id'] ) ? trim( (string) $args['location_id'] ) : '';
		if ( '' === $location_id && ! empty( $settings['location_id'] ) ) {
			$location_id = trim( (string) $settings['location_id'] );
		}
		if ( '' === $location_id ) {
			return new WP_Error( 'shc4wc_no_location', 'Square location id missing.' );
		}

		$amount_minor = (int) round( (float) $amount * 100 );
		if ( $amount_minor <= 0 ) {
			return new WP_Error( 'shc4wc_invalid_amount', 'Payment amount must be greater than zero.' );
		}

		$currency    = strtoupper( sanitize_text_field( (string) $currency ) );
		$reference   = sanitize_text_field( (string) $reference );
		$description = isset( $args['description'] ) ? (string) $args['description'] : $reference;
		$line_name   = isset( $args['line_name'] ) ? (string) $args['line_name'] : $description;
		$return_args = isset( $args['return_args'] ) && is_array( $args['return_args'] ) ? $args['return_args'] : array();

		$redirect_query = array_merge(
			array( 'wc-api' => 'shc4wc_return' ),
			$return_args
		);

		$line_items = isset( $args['line_items'] ) && is_array( $args['line_items'] ) ? $args['line_items'] : array();
		if ( empty( $line_items ) ) {
			$line_items = array(
				array(
					'name'             => $line_name,
					'quantity'         => '1',
					'base_price_money' => array(
						'amount'   => $amount_minor,
						'currency' => $currency,
					),
				),
			);
		}

		$checkout_extra = $this->maybe_app_fee_checkout_option( (float) $amount, $currency );
		if ( ! empty( $args['ask_for_shipping_address'] ) ) {
			$checkout_extra['ask_for_shipping_address'] = true;
		}
		if ( ! empty( $args['shipping_fee'] ) && is_array( $args['shipping_fee'] ) ) {
			$checkout_extra['shipping_fee'] = $args['shipping_fee'];
		}

		$payload = array(
			'idempotency_key'  => wp_generate_uuid4(),
			'description'      => $description,
			'order'            => array(
				'location_id'  => $location_id,
				'reference_id' => $reference,
				'line_items'   => $line_items,
			),
			'checkout_options' => $this->build_checkout_options( $redirect_query, $checkout_extra ),
			'payment_note'     => $description,
		);

		$payload = $this->attach_buyer_to_payment_link_payload( $payload, $args, true );
		$payload = $this->attach_buyer_address_to_payment_link_payload( $payload, $args );

		return $this->request( 'POST', '/v2/online-checkout/payment-links', $payload );
	}

	/**
	 * Create a Square hosted checkout link for a subscription plan variation.
	 *
	 * @see https://developer.squareup.com/docs/checkout-api/subscription-plan-checkout
	 *
	 * @param float  $amount              Initial phase price (major units).
	 * @param string $currency            ISO currency code.
	 * @param string $reference           Unique reference id.
	 * @param string $plan_variation_id   Square subscription plan *variation* id.
	 * @param array  $args                description, line_name, location_id, return_args.
	 * @return array|WP_Error
	 */
	public function create_subscription_payment_link( $amount, $currency, $reference, $plan_variation_id, $args = array() ) {
		$settings    = $this->settings;
		$location_id = isset( $args['location_id'] ) ? trim( (string) $args['location_id'] ) : '';
		if ( '' === $location_id && ! empty( $settings['location_id'] ) ) {
			$location_id = trim( (string) $settings['location_id'] );
		}
		if ( '' === $location_id ) {
			return new WP_Error( 'shc4wc_no_location', 'Square location id missing.' );
		}

		$plan_variation_id = sanitize_text_field( (string) $plan_variation_id );
		if ( '' === $plan_variation_id ) {
			return new WP_Error( 'shc4wc_no_subscription_plan', 'Square subscription plan variation id missing.' );
		}

		$amount_minor = (int) round( (float) $amount * 100 );
		if ( $amount_minor <= 0 ) {
			return new WP_Error( 'shc4wc_invalid_amount', 'Subscription amount must be greater than zero.' );
		}

		$currency    = strtoupper( sanitize_text_field( (string) $currency ) );
		$reference   = sanitize_text_field( (string) $reference );
		$description = isset( $args['description'] ) ? (string) $args['description'] : $reference;
		$line_name   = isset( $args['line_name'] ) ? (string) $args['line_name'] : $description;
		$return_args = isset( $args['return_args'] ) && is_array( $args['return_args'] ) ? $args['return_args'] : array();

		$redirect_query = array_merge(
			array( 'wc-api' => 'shc4wc_return' ),
			$return_args
		);

		$payload = array(
			'idempotency_key'  => wp_generate_uuid4(),
			'description'      => $description,
			'quick_pay'        => array(
				'location_id' => $location_id,
				'name'        => $line_name,
				'price_money' => array(
					'amount'   => $amount_minor,
					'currency' => $currency,
				),
			),
			'checkout_options' => $this->build_checkout_options(
				$redirect_query,
				array(
					'subscription_plan_id' => $plan_variation_id,
				)
			),
		);

		// quick_pay does not accept order.customer_id; pre-populate buyer so Square can match/create.
		$buyer = $this->normalize_buyer_args( $args );
		if ( '' !== $buyer['email'] ) {
			// Ensure a Square customer exists with this email for directory continuity.
			$this->resolve_or_create_customer_id( $buyer );
			$payload = $this->attach_pre_populated_data( $payload, $buyer );
		}

		return $this->request( 'POST', '/v2/online-checkout/payment-links', $payload );
	}

	/**
	 * Find a catalog item variation by exact SKU match.
	 *
	 * @param string $sku Catalog item variation SKU.
	 * @return array|WP_Error
	 */
	public function search_catalog_variation_by_sku( $sku ) {
		$sku = trim( (string) $sku );
		if ( '' === $sku ) {
			return new WP_Error( 'shc4wc_empty_sku', 'SKU is empty.' );
		}

		return $this->request(
			'POST',
			'/v2/catalog/search',
			array(
				'object_types' => array( 'ITEM_VARIATION' ),
				'limit'        => 1,
				'query'        => array(
					'exact_query' => array(
						'attribute_name'  => 'sku',
						'attribute_value' => $sku,
					),
				),
			)
		);
	}

	/**
	 * Search Square catalog subscription plan variations.
	 *
	 * @param string $cursor Pagination cursor.
	 * @return array|WP_Error
	 */
	public function search_subscription_plan_variations( $cursor = '' ) {
		$body = array(
			'object_types'            => array( 'SUBSCRIPTION_PLAN_VARIATION' ),
			'include_related_objects' => true,
		);
		if ( '' !== (string) $cursor ) {
			$body['cursor'] = (string) $cursor;
		}

		return $this->request( 'POST', '/v2/catalog/search', $body );
	}

	public function create_payment_link_for_order( WC_Order $order, $location_id ) {
		$currency     = $order->get_currency();
		$line_items   = $this->build_wc_order_line_items( $order, $currency );
		$shipping_fee = $this->build_square_shipping_fee_from_order( $order, $currency );

		$args = array(
			'location_id' => $location_id,
			'description' => 'WooCommerce Order #' . $order->get_id(),
			'line_items'  => $line_items,
			'return_args' => array(
				'order_id'  => $order->get_id(),
				'order_key' => $order->get_order_key(),
			),
			'customer'    => array(
				'email'           => (string) $order->get_billing_email(),
				'given_name'      => (string) $order->get_billing_first_name(),
				'family_name'     => (string) $order->get_billing_last_name(),
				'phone'           => (string) $order->get_billing_phone(),
				'billing_country' => (string) $order->get_billing_country(),
				'wp_user_id'      => (int) $order->get_user_id(),
			),
		);

		if ( ! empty( $shipping_fee ) ) {
			$args['shipping_fee'] = $shipping_fee;
		}

		if ( $this->order_should_ask_for_shipping_address( $order ) ) {
			$args['ask_for_shipping_address'] = true;
			$address                         = $this->build_square_buyer_address_from_order( $order );
			if ( ! empty( $address ) ) {
				$args['buyer_address'] = $address;
			}
		}

		return $this->create_payment_link(
			(float) $order->get_total(),
			$currency,
			'wc_order_' . $order->get_id(),
			$args
		);
	}

	/**
	 * Whether Square Hosted Checkout should collect / show shipping (SHIPMENT fulfillment).
	 *
	 * Skips virtual-only orders and local pickup so Square does not show "Pickup from…".
	 *
	 * @param WC_Order $order WooCommerce order.
	 * @return bool
	 */
	public function order_should_ask_for_shipping_address( WC_Order $order ) {
		$should = false;

		if ( method_exists( $order, 'needs_shipping_address' ) && $order->needs_shipping_address() ) {
			$should = ! $this->order_uses_local_pickup( $order );
		}

		/**
		 * Filter whether CreatePaymentLink sets ask_for_shipping_address for a Woo order.
		 *
		 * @param bool     $should Request shipping on Square checkout.
		 * @param WC_Order $order  Order.
		 */
		return (bool) apply_filters( 'shc4wc_order_ask_for_shipping_address', $should, $order );
	}

	/**
	 * @param WC_Order $order WooCommerce order.
	 * @return bool
	 */
	private function order_uses_local_pickup( WC_Order $order ) {
		$methods = $order->get_shipping_methods();
		if ( empty( $methods ) ) {
			return false;
		}

		foreach ( $methods as $method ) {
			$id = strtolower( (string) $method->get_method_id() );
			if ( '' === $id ) {
				continue;
			}
			if ( 'local_pickup' === $id || 'pickup_location' === $id || false !== strpos( $id, 'local_pickup' ) ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Map WooCommerce order address to Square Address for pre_populated_data.buyer_address.
	 *
	 * Prefers shipping fields; falls back to billing when shipping is empty.
	 *
	 * @param WC_Order $order WooCommerce order.
	 * @return array<string, string>
	 */
	private function build_square_buyer_address_from_order( WC_Order $order ) {
		$line1 = trim( (string) $order->get_shipping_address_1() );
		if ( '' === $line1 ) {
			$line1 = trim( (string) $order->get_billing_address_1() );
		}

		$line2 = trim( (string) $order->get_shipping_address_2() );
		if ( '' === $line2 ) {
			$line2 = trim( (string) $order->get_billing_address_2() );
		}

		$city = trim( (string) $order->get_shipping_city() );
		if ( '' === $city ) {
			$city = trim( (string) $order->get_billing_city() );
		}

		$state = trim( (string) $order->get_shipping_state() );
		if ( '' === $state ) {
			$state = trim( (string) $order->get_billing_state() );
		}

		$postal = trim( (string) $order->get_shipping_postcode() );
		if ( '' === $postal ) {
			$postal = trim( (string) $order->get_billing_postcode() );
		}

		$country = strtoupper( trim( (string) $order->get_shipping_country() ) );
		if ( '' === $country ) {
			$country = strtoupper( trim( (string) $order->get_billing_country() ) );
		}

		if ( '' === $line1 || '' === $country ) {
			return array();
		}

		$address = array(
			'address_line_1'                  => sanitize_text_field( $line1 ),
			'locality'                        => sanitize_text_field( $city ),
			'administrative_district_level_1' => sanitize_text_field( $state ),
			'postal_code'                     => sanitize_text_field( $postal ),
			'country'                         => sanitize_text_field( $country ),
		);

		if ( '' !== $line2 ) {
			$address['address_line_2'] = sanitize_text_field( $line2 );
		}

		/**
		 * Filter Square buyer_address built from a WooCommerce order.
		 *
		 * @param array<string, string> $address Square Address fields.
		 * @param WC_Order              $order   Order.
		 */
		$filtered = apply_filters( 'shc4wc_square_buyer_address_from_order', $address, $order );
		return is_array( $filtered ) ? $filtered : $address;
	}

	/**
	 * Find or create a Square customer and return its ID.
	 *
	 * @param array<string, mixed> $buyer email, given_name, family_name, phone, wp_user_id, customer_id.
	 * @return string Square customer ID or empty string on failure / missing email.
	 */
	public function resolve_or_create_customer_id( array $buyer ) {
		$buyer = $this->normalize_buyer_args( array( 'customer' => $buyer ) );
		$email = $buyer['email'];
		if ( '' === $email ) {
			return '';
		}

		if ( '' !== $buyer['customer_id'] ) {
			return $buyer['customer_id'];
		}

		$wp_user_id = (int) $buyer['wp_user_id'];
		if ( $wp_user_id > 0 ) {
			$meta_id = sanitize_text_field( (string) get_user_meta( $wp_user_id, '_square_customer_id', true ) );
			if ( '' !== $meta_id ) {
				return $meta_id;
			}
		}

		$found = $this->find_customer_id_by_email( $email );
		if ( is_wp_error( $found ) ) {
			$this->log( 'Square customer search failed', array( 'error' => $found->get_error_data(), 'email' => $email ), 'error' );
			return '';
		}
		if ( '' !== $found ) {
			if ( $wp_user_id > 0 ) {
				update_user_meta( $wp_user_id, '_square_customer_id', $found );
			}
			return $found;
		}

		$body = array(
			'idempotency_key' => 'shc4wc-pay-customer-' . wp_hash( strtolower( $email ) . '|' . (string) $wp_user_id ),
			'email_address'   => $email,
		);
		if ( '' !== $buyer['given_name'] ) {
			$body['given_name'] = $buyer['given_name'];
		}
		if ( '' !== $buyer['family_name'] ) {
			$body['family_name'] = $buyer['family_name'];
		}
		if ( '' !== $buyer['phone'] ) {
			$body['phone_number'] = $buyer['phone'];
		}
		if ( empty( $body['given_name'] ) && empty( $body['family_name'] ) ) {
			$body['given_name'] = $email;
		}
		if ( $wp_user_id > 0 ) {
			$body['reference_id'] = 'wp-' . $wp_user_id;
		}

		$response = $this->request( 'POST', '/v2/customers', $body );
		if ( is_wp_error( $response ) ) {
			// Race: customer may already exist — search again.
			$retry = $this->find_customer_id_by_email( $email );
			if ( ! is_wp_error( $retry ) && '' !== $retry ) {
				if ( $wp_user_id > 0 ) {
					update_user_meta( $wp_user_id, '_square_customer_id', $retry );
				}
				return $retry;
			}
			$this->log( 'Square customer create failed', array( 'error' => $response->get_error_data(), 'email' => $email ), 'error' );
			return '';
		}

		$square_id = isset( $response['customer']['id'] ) ? sanitize_text_field( (string) $response['customer']['id'] ) : '';
		if ( '' !== $square_id && $wp_user_id > 0 ) {
			update_user_meta( $wp_user_id, '_square_customer_id', $square_id );
		}

		return $square_id;
	}

	/**
	 * Search Square Customers by exact email.
	 *
	 * @param string $email Buyer email.
	 * @return string|WP_Error Square customer ID, empty string if none, or WP_Error.
	 */
	public function find_customer_id_by_email( $email ) {
		$email = self::normalize_email_for_square( $email );
		if ( '' === $email ) {
			return '';
		}

		$response = $this->request(
			'POST',
			'/v2/customers/search',
			array(
				'limit' => 1,
				'query' => array(
					'filter' => array(
						'email_address' => array(
							'exact' => $email,
						),
					),
				),
			)
		);

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

		if ( empty( $response['customers'][0]['id'] ) ) {
			return '';
		}

		return sanitize_text_field( (string) $response['customers'][0]['id'] );
	}

	/**
	 * Attach customer_id (on order) and pre_populated_data from buyer args.
	 *
	 * @param array<string, mixed> $payload CreatePaymentLink body.
	 * @param array<string, mixed> $args    Caller args (customer / buyer_*).
	 * @param bool                 $on_order Whether to set order.customer_id.
	 * @return array<string, mixed>
	 */
	private function attach_buyer_to_payment_link_payload( array $payload, array $args, $on_order = true ) {
		$buyer = $this->normalize_buyer_args( $args );
		if ( '' === $buyer['email'] && '' === $buyer['customer_id'] ) {
			return $payload;
		}

		$customer_id = $this->resolve_or_create_customer_id( $buyer );
		if ( $on_order && '' !== $customer_id && isset( $payload['order'] ) && is_array( $payload['order'] ) ) {
			$payload['order']['customer_id'] = $customer_id;
		}

		return $this->attach_pre_populated_data( $payload, $buyer );
	}

	/**
	 * Format a phone number for Square (E.164, max 17 chars).
	 *
	 * @param string $phone   Raw phone input.
	 * @param string $country ISO 3166-1 alpha-2 buyer or store country.
	 * @return string
	 */
	private function format_phone_for_square( $phone, $country = '' ) {
		$phone = sanitize_text_field( (string) $phone );
		if ( '' === $phone ) {
			return '';
		}

		$digits = preg_replace( '/[^\d+]/', '', $phone );
		if ( ! is_string( $digits ) || '' === $digits ) {
			return '';
		}

		if ( 0 === strpos( $digits, '+' ) ) {
			$e164 = $digits;
		} else {
			$country = strtoupper( sanitize_text_field( (string) $country ) );
			if ( '' === $country && function_exists( 'WC' ) && WC()->countries ) {
				$country = (string) WC()->countries->get_base_country();
			}
			if ( '' === $country ) {
				$country = 'US';
			}

			$numeric = preg_replace( '/\D/', '', $digits );
			if ( ! is_string( $numeric ) || '' === $numeric ) {
				return '';
			}

			$calling_code = $this->get_country_calling_code( $country );
			if ( '' === $calling_code ) {
				return '';
			}

			// NANP: require a valid 10-digit national number (or 11 with leading 1).
			if ( '1' === $calling_code ) {
				if ( 10 === strlen( $numeric ) ) {
					$e164 = '+1' . $numeric;
				} elseif ( 11 === strlen( $numeric ) && '1' === $numeric[0] ) {
					$e164 = '+' . $numeric;
				} else {
					return '';
				}
			} else {
				if ( '0' === $numeric[0] ) {
					$numeric = substr( $numeric, 1 );
				}
				if ( 0 === strpos( $numeric, $calling_code ) ) {
					$e164 = '+' . $numeric;
				} else {
					$e164 = '+' . $calling_code . $numeric;
				}
			}
		}

		if ( strlen( $e164 ) > 17 || ! preg_match( '/^\+\d{8,16}$/', $e164 ) ) {
			return '';
		}

		return $e164;
	}

	/**
	 * @param string $country ISO 3166-1 alpha-2 country code.
	 * @return string Digits-only calling code.
	 */
	private function get_country_calling_code( $country ) {
		if ( function_exists( 'WC' ) && WC()->countries && method_exists( WC()->countries, 'get_country_calling_code' ) ) {
			$code = WC()->countries->get_country_calling_code( $country );
			if ( is_string( $code ) && '' !== $code ) {
				$digits = preg_replace( '/\D/', '', $code );
				return is_string( $digits ) ? $digits : '';
			}
		}

		$fallback = array(
			'US' => '1',
			'CA' => '1',
			'GB' => '44',
			'AU' => '61',
			'NZ' => '64',
			'IE' => '353',
		);

		return isset( $fallback[ $country ] ) ? $fallback[ $country ] : '';
	}

	/**
	 * @param array<string, mixed> $payload CreatePaymentLink body.
	 * @param array<string, string> $buyer  Normalized buyer fields.
	 * @return array<string, mixed>
	 */
	private function attach_pre_populated_data( array $payload, array $buyer ) {
		$pre = array();
		if ( '' !== $buyer['email'] ) {
			$pre['buyer_email'] = $buyer['email'];
		}
		if ( '' !== $buyer['phone'] ) {
			$pre['buyer_phone_number'] = $buyer['phone'];
		}
		if ( ! empty( $pre ) ) {
			$payload['pre_populated_data'] = isset( $payload['pre_populated_data'] ) && is_array( $payload['pre_populated_data'] )
				? array_merge( $payload['pre_populated_data'], $pre )
				: $pre;
		}

		return $payload;
	}

	/**
	 * Attach Square buyer_address for shipping fulfillment prefill.
	 *
	 * @param array<string, mixed> $payload CreatePaymentLink body.
	 * @param array<string, mixed> $args    Caller args (buyer_address).
	 * @return array<string, mixed>
	 */
	private function attach_buyer_address_to_payment_link_payload( array $payload, array $args ) {
		$raw = isset( $args['buyer_address'] ) && is_array( $args['buyer_address'] ) ? $args['buyer_address'] : array();
		if ( empty( $raw ) ) {
			return $payload;
		}

		$address = array();
		$map     = array(
			'address_line_1'                  => 'address_line_1',
			'address_line_2'                  => 'address_line_2',
			'locality'                        => 'locality',
			'administrative_district_level_1' => 'administrative_district_level_1',
			'postal_code'                     => 'postal_code',
			'country'                         => 'country',
		);

		foreach ( $map as $key => $_ ) {
			if ( empty( $raw[ $key ] ) || ! is_scalar( $raw[ $key ] ) ) {
				continue;
			}
			$value = sanitize_text_field( (string) $raw[ $key ] );
			if ( '' === $value ) {
				continue;
			}
			if ( 'country' === $key ) {
				$value = strtoupper( $value );
			}
			$address[ $key ] = $value;
		}

		if ( empty( $address['address_line_1'] ) || empty( $address['country'] ) ) {
			return $payload;
		}

		$payload['pre_populated_data'] = isset( $payload['pre_populated_data'] ) && is_array( $payload['pre_populated_data'] )
			? $payload['pre_populated_data']
			: array();
		$payload['pre_populated_data']['buyer_address'] = $address;

		return $payload;
	}

	/**
	 * Normalize buyer details from create_payment_link args.
	 *
	 * Accepts either a nested `customer` array or flat buyer_* / email keys.
	 *
	 * @param array<string, mixed> $args Caller args.
	 * @return array{email:string,given_name:string,family_name:string,phone:string,wp_user_id:int,customer_id:string}
	 */
	private function normalize_buyer_args( array $args ) {
		$customer = isset( $args['customer'] ) && is_array( $args['customer'] ) ? $args['customer'] : $args;

		$email = '';
		if ( ! empty( $customer['email'] ) ) {
			$email = sanitize_email( (string) $customer['email'] );
		} elseif ( ! empty( $customer['buyer_email'] ) ) {
			$email = sanitize_email( (string) $customer['buyer_email'] );
		} elseif ( ! empty( $args['buyer_email'] ) ) {
			$email = sanitize_email( (string) $args['buyer_email'] );
		}
		if ( '' !== $email ) {
			$email = self::normalize_email_for_square( $email );
		}

		$given = '';
		if ( ! empty( $customer['given_name'] ) ) {
			$given = sanitize_text_field( (string) $customer['given_name'] );
		} elseif ( ! empty( $customer['first_name'] ) ) {
			$given = sanitize_text_field( (string) $customer['first_name'] );
		}

		$family = '';
		if ( ! empty( $customer['family_name'] ) ) {
			$family = sanitize_text_field( (string) $customer['family_name'] );
		} elseif ( ! empty( $customer['last_name'] ) ) {
			$family = sanitize_text_field( (string) $customer['last_name'] );
		}

		$phone = '';
		if ( ! empty( $customer['phone'] ) ) {
			$phone = sanitize_text_field( (string) $customer['phone'] );
		} elseif ( ! empty( $customer['phone_number'] ) ) {
			$phone = sanitize_text_field( (string) $customer['phone_number'] );
		} elseif ( ! empty( $customer['buyer_phone_number'] ) ) {
			$phone = sanitize_text_field( (string) $customer['buyer_phone_number'] );
		}

		$country = '';
		if ( ! empty( $customer['billing_country'] ) ) {
			$country = sanitize_text_field( (string) $customer['billing_country'] );
		} elseif ( ! empty( $customer['country'] ) ) {
			$country = sanitize_text_field( (string) $customer['country'] );
		}
		if ( '' !== $phone ) {
			$phone = $this->format_phone_for_square( $phone, $country );
		}

		$wp_user_id = 0;
		if ( ! empty( $customer['wp_user_id'] ) ) {
			$wp_user_id = (int) $customer['wp_user_id'];
		} elseif ( ! empty( $args['wp_user_id'] ) ) {
			$wp_user_id = (int) $args['wp_user_id'];
		}

		$customer_id = '';
		if ( ! empty( $customer['customer_id'] ) ) {
			$customer_id = sanitize_text_field( (string) $customer['customer_id'] );
		} elseif ( ! empty( $args['customer_id'] ) ) {
			$customer_id = sanitize_text_field( (string) $args['customer_id'] );
		}

		return array(
			'email'       => $email,
			'given_name'  => $given,
			'family_name' => $family,
			'phone'       => $phone,
			'wp_user_id'  => $wp_user_id,
			'customer_id' => $customer_id,
		);
	}

	/**
	 * Build Square checkout_options.shipping_fee from WooCommerce shipping totals.
	 *
	 * Square applies this as an Order service charge (not a line item).
	 *
	 * @param WC_Order $order    WooCommerce order.
	 * @param string   $currency ISO currency code.
	 * @return array<string, mixed> Empty when shipping is free / zero.
	 */
	private function build_square_shipping_fee_from_order( WC_Order $order, $currency ) {
		$shipping_total = (float) $order->get_shipping_total() + (float) $order->get_shipping_tax();
		if ( $shipping_total <= 0 ) {
			return array();
		}

		$currency = strtoupper( sanitize_text_field( (string) $currency ) );
		if ( 3 !== strlen( $currency ) ) {
			$currency = 'USD';
		}

		$name = __( 'Shipping', 'payments-connect-square' );
		foreach ( $order->get_shipping_methods() as $method ) {
			$method_name = wp_strip_all_tags( (string) $method->get_name() );
			if ( '' !== $method_name ) {
				$name = $method_name;
				break;
			}
		}

		/**
		 * Filter Square checkout_options.shipping_fee for a WooCommerce order.
		 *
		 * Return empty array to omit shipping_fee (caller must ensure totals still match).
		 *
		 * @param array<string, mixed> $shipping_fee Square ShippingFee payload.
		 * @param WC_Order             $order        Order.
		 */
		$shipping_fee = apply_filters(
			'shc4wc_square_shipping_fee',
			array(
				'name'   => $name,
				'charge' => array(
					'amount'   => (int) round( $shipping_total * 100 ),
					'currency' => $currency,
				),
			),
			$order
		);

		if ( ! is_array( $shipping_fee ) || empty( $shipping_fee['charge']['amount'] ) ) {
			return array();
		}

		return $shipping_fee;
	}

	/**
	 * Build Square order line items from a WooCommerce order (products + fees).
	 *
	 * Paid shipping is sent separately via checkout_options.shipping_fee (service charge).
	 *
	 * @param WC_Order $order    WooCommerce order.
	 * @param string   $currency ISO currency code.
	 * @return array
	 */
	private function build_wc_order_line_items( WC_Order $order, $currency ) {
		$currency   = strtoupper( sanitize_text_field( (string) $currency ) );
		$line_items = array();

		foreach ( $order->get_items() as $item ) {
			if ( ! $item instanceof WC_Order_Item_Product ) {
				continue;
			}

			$quantity = max( 1, (int) $item->get_quantity() );
			$total    = (float) $item->get_total() + (float) $item->get_total_tax();
			if ( $total <= 0 ) {
				continue;
			}

			$product    = $item->get_product();
			$sku        = $product ? trim( (string) $product->get_sku() ) : '';
			$catalog_id = '' !== $sku ? SHC4WC_Catalog::find_variation_id_by_sku( $sku ) : '';

			$line_items[] = $this->make_square_line_item(
				wp_strip_all_tags( $item->get_name() ),
				$quantity,
				(int) round( ( $total / $quantity ) * 100 ),
				$currency,
				$catalog_id
			);
		}

		foreach ( $order->get_fees() as $fee ) {
			if ( ! $fee instanceof WC_Order_Item_Fee ) {
				continue;
			}

			$fee_total = (float) $fee->get_total() + (float) $fee->get_total_tax();
			if ( $fee_total <= 0 ) {
				continue;
			}

			$line_items[] = $this->make_square_line_item(
				wp_strip_all_tags( $fee->get_name() ),
				1,
				(int) round( $fee_total * 100 ),
				$currency
			);
		}

		$shipping_minor = (int) round( ( (float) $order->get_shipping_total() + (float) $order->get_shipping_tax() ) * 100 );
		$order_minor    = (int) round( (float) $order->get_total() * 100 );
		$expected_minor = max( 0, $order_minor - max( 0, $shipping_minor ) );

		if ( empty( $line_items ) ) {
			if ( $expected_minor > 0 ) {
				$line_items[] = $this->make_square_line_item(
					'Order #' . $order->get_order_number(),
					1,
					$expected_minor,
					$currency
				);
			}
			return $line_items;
		}

		return $this->reconcile_line_items_total( $line_items, $expected_minor, $currency );
	}

	/**
	 * @param string $name     Line item display name.
	 * @param int    $quantity Quantity.
	 * @param int    $unit_minor Unit price in minor currency units.
	 * @param string $currency ISO currency code.
	 * @param string $catalog_object_id Optional Square CatalogItemVariation id (from SKU match).
	 * @return array
	 */
	private function make_square_line_item( $name, $quantity, $unit_minor, $currency, $catalog_object_id = '' ) {
		$name = sanitize_text_field( (string) $name );
		if ( '' === $name ) {
			$name = __( 'Item', 'payments-connect-square' );
		}

		$line_item = array(
			'name'             => $name,
			'quantity'         => (string) max( 1, (int) $quantity ),
			'base_price_money' => array(
				'amount'   => max( 0, (int) $unit_minor ),
				'currency' => strtoupper( sanitize_text_field( (string) $currency ) ),
			),
		);

		$catalog_object_id = sanitize_text_field( (string) $catalog_object_id );
		if ( '' !== $catalog_object_id ) {
			$line_item['catalog_object_id'] = $catalog_object_id;
		}

		return $line_item;
	}

	/**
	 * Adjust the last line item when rounding leaves a small total mismatch.
	 *
	 * @param array  $line_items    Square line items.
	 * @param int    $expected_minor Expected order total in minor units.
	 * @param string $currency      ISO currency code.
	 * @return array
	 */
	private function reconcile_line_items_total( array $line_items, $expected_minor, $currency ) {
		$computed = 0;
		foreach ( $line_items as $line_item ) {
			$qty      = isset( $line_item['quantity'] ) ? (int) $line_item['quantity'] : 1;
			$amount   = isset( $line_item['base_price_money']['amount'] ) ? (int) $line_item['base_price_money']['amount'] : 0;
			$computed += $amount * max( 1, $qty );
		}

		$diff = $expected_minor - $computed;
		if ( 0 === $diff ) {
			return $line_items;
		}

		if ( abs( $diff ) <= 99 ) {
			$last_index = count( $line_items ) - 1;
			$qty        = max( 1, (int) $line_items[ $last_index ]['quantity'] );
			$line_items[ $last_index ]['base_price_money']['amount'] += (int) round( $diff / $qty );
			return $line_items;
		}

		$line_items[] = $this->make_square_line_item(
			__( 'Order adjustment', 'payments-connect-square' ),
			1,
			$diff,
			$currency
		);

		return $line_items;
	}

	/**
	 * Load a Square Order by id (used after buyer return to read tenders → payment id without webhooks).
	 *
	 * @param string $square_order_id Square Order id from payment-link response.
	 * @return array|WP_Error
	 */
	public function retrieve_order( $square_order_id ) {
		$square_order_id = sanitize_text_field( (string) $square_order_id );
		if ( '' === $square_order_id ) {
			return new WP_Error( 'shc4wc_no_square_order', 'Square order id missing.' );
		}
		return $this->request( 'GET', '/v2/orders/' . rawurlencode( $square_order_id ) );
	}

	/**
	 * Retrieve a catalog object (item variation SKU lookup for inventory webhooks).
	 *
	 * @param string $catalog_object_id Square catalog object id.
	 * @return array|WP_Error
	 */
	public function retrieve_catalog_object( $catalog_object_id ) {
		$catalog_object_id = sanitize_text_field( (string) $catalog_object_id );
		if ( '' === $catalog_object_id ) {
			return new WP_Error( 'shc4wc_no_catalog_object', 'Catalog object id missing.' );
		}

		return $this->request( 'GET', '/v2/catalog/object/' . rawurlencode( $catalog_object_id ) );
	}

	/**
	 * Retrieve an Online Checkout payment link (after buyer return, related orders may include tenders sooner than GET /v2/orders alone).
	 *
	 * @param string $payment_link_id Payment link id from create response.
	 * @return array|WP_Error
	 */
	public function retrieve_payment_link( $payment_link_id ) {
		$payment_link_id = sanitize_text_field( (string) $payment_link_id );
		if ( '' === $payment_link_id ) {
			return new WP_Error( 'shc4wc_no_payment_link', 'Square payment link id missing.' );
		}
		return $this->request( 'GET', '/v2/online-checkout/payment-links/' . rawurlencode( $payment_link_id ) );
	}

	/**
	 * List payments for a location (GET /v2/payments). Square does not expose POST /v2/payments/search on Connect; match client-side by order_id.
	 *
	 * @param string $location_id Location id.
	 * @param string $begin_time_rfc3339 Inclusive lower bound (UTC RFC3339).
	 * @param string $cursor Pagination cursor from a previous response, or empty.
	 * @return array|WP_Error
	 */
	public function list_recent_payments( $location_id, $begin_time_rfc3339, $cursor = '' ) {
		$location_id = sanitize_text_field( (string) $location_id );
		if ( '' === $location_id ) {
			return new WP_Error( 'shc4wc_no_location', 'Square location id missing.' );
		}
		$args = array(
			'location_id' => $location_id,
			'begin_time'  => $begin_time_rfc3339,
			'limit'       => 100,
			'sort_order'  => 'DESC',
			'sort_field'  => 'CREATED_AT',
		);
		if ( '' !== (string) $cursor ) {
			$args['cursor'] = (string) $cursor;
		}
		$url = add_query_arg(
			$args,
			trailingslashit( $this->get_api_base() ) . 'v2/payments'
		);

		$response = wp_remote_get(
			$url,
			array(
				'headers' => $this->get_square_auth_headers(),
				'timeout' => 60,
			)
		);

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

		$code = (int) wp_remote_retrieve_response_code( $response );
		$raw  = wp_remote_retrieve_body( $response );
		$data = json_decode( $raw, true );
		if ( ! is_array( $data ) ) {
			$data = array();
		}

		if ( 401 === $code && $this->should_retry_after_auth_failure( $data ) ) {
			$refreshed = SHC4WC_OAuth_Token::refresh_access_token( true );
			if ( ! is_wp_error( $refreshed ) && '' !== $refreshed ) {
				return $this->list_recent_payments( $location_id, $begin_time_rfc3339, $cursor );
			}
		}

		if ( $code < 200 || $code >= 300 ) {
			return new WP_Error(
				'shc4wc_square_list_payments_error',
				'Square list payments failed',
				array(
					'status' => $code,
					'data'   => $data,
				)
			);
		}

		return $data;
	}

	/**
	 * Build checkout_options for CreatePaymentLink, including accepted payment methods.
	 *
	 * Apple Pay and Google Pay appear by default on Square checkout, but Cash App Pay and
	 * Afterpay/Clearpay must be explicitly opted in per payment link.
	 *
	 * @param array<string, mixed> $redirect_query Query args for the buyer return URL.
	 * @param array<string, mixed> $extra          Additional checkout_options fields.
	 * @return array<string, mixed>
	 */
	private function build_checkout_options( array $redirect_query, array $extra = array() ) {
		return array_merge(
			array(
				'redirect_url'             => $this->normalize_return_url( add_query_arg( $redirect_query, home_url( '/' ) ) ),
				'accepted_payment_methods' => $this->get_accepted_payment_methods(),
			),
			$extra
		);
	}

	/**
	 * Hostnames allowed to collect Square application fees (after stripping www).
	 *
	 * @return string[]
	 */
	public static function get_app_fee_hosts() {
		/**
		 * Filter hosts that may send checkout_options.app_fee_money.
		 *
		 * Default is empty (no site collects an application fee). Opt in per site
		 * with a small mu-plugin or theme code, e.g.:
		 *
		 * add_filter( 'shc4wc_app_fee_hosts', function( $hosts ) {
		 *     $hosts[] = 'example.com';
		 *     return $hosts;
		 * } );
		 *
		 * @param string[] $hosts Hostnames without leading www.
		 */
		$hosts = apply_filters( 'shc4wc_app_fee_hosts', array() );
		if ( ! is_array( $hosts ) ) {
			return array();
		}
		$clean = array();
		foreach ( $hosts as $host ) {
			$host = strtolower( preg_replace( '/^www\./', '', sanitize_text_field( (string) $host ) ) );
			if ( '' !== $host ) {
				$clean[] = $host;
			}
		}
		return array_values( array_unique( $clean ) );
	}

	/**
	 * Whether this WordPress site may collect an application fee on payment links.
	 *
	 * @return bool
	 */
	public static function site_allows_app_fee() {
		$host = wp_parse_url( home_url( '/' ), PHP_URL_HOST );
		$host = is_string( $host ) ? strtolower( preg_replace( '/^www\./', '', $host ) ) : '';
		if ( '' === $host ) {
			return false;
		}
		return in_array( $host, self::get_app_fee_hosts(), true );
	}

	/**
	 * Application fee percent of the payment total (e.g. 0.5 = half a percent).
	 *
	 * @return float
	 */
	public static function get_app_fee_percent() {
		/**
		 * Filter Square application fee percent (of payment total).
		 *
		 * Default is 0 (disabled). Set a positive value (e.g. 0.5 for half a percent)
		 * only on sites that also opt in via shc4wc_app_fee_hosts.
		 *
		 * @param float $percent Fee percent, e.g. 0.5 for 0.5%.
		 */
		$percent = (float) apply_filters( 'shc4wc_app_fee_percent', 0.0 );
		if ( $percent <= 0 || $percent > 90 ) {
			return 0.0;
		}
		return $percent;
	}

	/**
	 * Optional checkout_options.app_fee_money for allowlisted live sites only.
	 *
	 * Not used for subscription plan checkout (Square does not support app fees there).
	 *
	 * @param float  $amount_major Payment total in major currency units.
	 * @param string $currency     ISO currency code.
	 * @return array<string, mixed>
	 */
	private function maybe_app_fee_checkout_option( $amount_major, $currency ) {
		if ( 'live' !== $this->get_environment() || ! self::site_allows_app_fee() ) {
			return array();
		}

		$percent = self::get_app_fee_percent();
		if ( $percent <= 0 ) {
			return array();
		}

		$fee_minor = (int) round( (float) $amount_major * ( $percent / 100 ) * 100 );
		if ( $fee_minor < 1 ) {
			return array();
		}

		$total_minor = (int) round( (float) $amount_major * 100 );
		if ( $fee_minor >= $total_minor ) {
			return array();
		}

		$currency = strtoupper( sanitize_text_field( (string) $currency ) );
		if ( 3 !== strlen( $currency ) ) {
			$currency = 'USD';
		}

		$this->log(
			'Application fee attached to payment link',
			array(
				'amount_major' => $amount_major,
				'fee_minor'    => $fee_minor,
				'percent'      => $percent,
				'currency'     => $currency,
			)
		);

		return array(
			'app_fee_money' => array(
				'amount'   => $fee_minor,
				'currency' => $currency,
			),
		);
	}

	/**
	 * Ensure buyer return URLs work on local XAMPP (HTTP on port 80).
	 *
	 * @param string $url Return URL.
	 * @return string
	 */
	private function normalize_return_url( $url ) {
		$host = wp_parse_url( (string) $url, PHP_URL_HOST );
		if ( $host && in_array( $host, array( 'localhost', '127.0.0.1' ), true ) ) {
			return set_url_scheme( (string) $url, 'http' );
		}
		return (string) $url;
	}

	/**
	 * Map Square merchant checkout settings to CreatePaymentLink accepted_payment_methods.
	 *
	 * @return array<string, bool>
	 */
	public function get_accepted_payment_methods() {
		$cache_key = 'shc4wc_accepted_payment_methods_' . $this->get_environment();
		$cached    = get_transient( $cache_key );
		if ( is_array( $cached ) ) {
			return $cached;
		}

		$methods = array(
			'apple_pay'         => true,
			'google_pay'        => true,
			'cash_app_pay'      => false,
			'afterpay_clearpay' => false,
		);

		if ( 'live' === $this->get_environment() ) {
			$response = $this->retrieve_merchant_checkout_settings();
			if ( ! is_wp_error( $response ) && ! empty( $response['merchant_settings']['payment_methods'] ) ) {
				$payment_methods = $response['merchant_settings']['payment_methods'];
				if ( isset( $payment_methods['apple_pay']['enabled'] ) ) {
					$methods['apple_pay'] = (bool) $payment_methods['apple_pay']['enabled'];
				}
				if ( isset( $payment_methods['google_pay']['enabled'] ) ) {
					$methods['google_pay'] = (bool) $payment_methods['google_pay']['enabled'];
				}
				$cash_enabled = ! empty( $payment_methods['cash_app']['enabled'] ) || ! empty( $payment_methods['cash_app_pay']['enabled'] );
				$methods['cash_app_pay'] = $cash_enabled;
				if ( isset( $payment_methods['afterpay_clearpay']['enabled'] ) ) {
					$methods['afterpay_clearpay'] = (bool) $payment_methods['afterpay_clearpay']['enabled'];
				}
			}
		}

		set_transient( $cache_key, $methods, 5 * MINUTE_IN_SECONDS );

		return $methods;
	}

	/**
	 * Clear cached accepted payment methods (e.g. after merchant settings change).
	 */
	public function clear_accepted_payment_methods_cache() {
		delete_transient( 'shc4wc_accepted_payment_methods_live' );
		delete_transient( 'shc4wc_accepted_payment_methods_sandbox' );
	}

	/**
	 * Retrieve Square Checkout merchant settings.
	 *
	 * @return array|WP_Error
	 */
	public function retrieve_merchant_checkout_settings() {
		return $this->request( 'GET', '/v2/online-checkout/merchant-settings' );
	}

	/**
	 * Update Square Checkout merchant settings.
	 *
	 * @param array $merchant_settings Partial merchant_settings payload.
	 * @return array|WP_Error
	 */
	public function update_merchant_checkout_settings( array $merchant_settings ) {
		return $this->request(
			'PUT',
			'/v2/online-checkout/merchant-settings',
			array(
				'merchant_settings' => $merchant_settings,
			)
		);
	}

	/**
	 * Retrieve Square Checkout location settings.
	 *
	 * @param string $location_id Square location id.
	 * @return array|WP_Error
	 */
	public function retrieve_location_checkout_settings( $location_id ) {
		$location_id = sanitize_text_field( (string) $location_id );
		if ( '' === $location_id ) {
			return new WP_Error( 'shc4wc_no_location', 'Square location id missing.' );
		}

		return $this->request( 'GET', '/v2/online-checkout/location-settings/' . rawurlencode( $location_id ) );
	}

	/**
	 * Update Square Checkout location settings.
	 *
	 * @param string $location_id       Square location id.
	 * @param array  $location_settings Partial location_settings payload.
	 * @return array|WP_Error
	 */
	public function update_location_checkout_settings( $location_id, array $location_settings ) {
		$location_id = sanitize_text_field( (string) $location_id );
		if ( '' === $location_id ) {
			return new WP_Error( 'shc4wc_no_location', 'Square location id missing.' );
		}

		return $this->request(
			'PUT',
			'/v2/online-checkout/location-settings/' . rawurlencode( $location_id ),
			array(
				'location_settings' => $location_settings,
			)
		);
	}

	/**
	 * Refund a completed Square payment (full or partial).
	 *
	 * @param string $payment_id      Square Payment id stored on the WooCommerce order.
	 * @param float  $amount          Refund amount in major currency units.
	 * @param string $currency        ISO currency code.
	 * @param string $idempotency_key Unique key per refund attempt.
	 * @return array|WP_Error
	 */
	public function create_refund( $payment_id, $amount, $currency, $idempotency_key = '' ) {
		$payment_id = sanitize_text_field( (string) $payment_id );
		if ( '' === $payment_id ) {
			return new WP_Error( 'shc4wc_no_payment', __( 'Square payment ID is missing.', 'payments-connect-square' ) );
		}

		$amount_minor = (int) round( (float) $amount * 100 );
		if ( $amount_minor <= 0 ) {
			return new WP_Error( 'shc4wc_invalid_refund_amount', __( 'Refund amount must be greater than zero.', 'payments-connect-square' ) );
		}

		$currency = strtoupper( sanitize_text_field( (string) $currency ) );
		if ( 3 !== strlen( $currency ) ) {
			$currency = 'USD';
		}

		if ( '' === (string) $idempotency_key ) {
			$idempotency_key = wp_generate_uuid4();
		}

		return $this->request(
			'POST',
			'/v2/refunds',
			array(
				'idempotency_key' => sanitize_text_field( (string) $idempotency_key ),
				'amount_money'    => array(
					'amount'   => $amount_minor,
					'currency' => $currency,
				),
				'payment_id'      => $payment_id,
			)
		);
	}
}
