<?php
/**
 * Helper functions.
 *
 * @since 1.0.0
 */

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

if ( ! function_exists( 'sbuy_get_options' ) ) {
	/**
	 * Get options
	 *
	 * @param int|string $key option key.
	 * @param string $options_name options name.
	 *
	 * @since 1.0.0
	 */
	function sbuy_get_options( $key, string $options_name ) {

		$options = get_option( $options_name );

		return $options[ $key ] ?? '';
	}
}


if ( ! function_exists( 'sbuy_convert_cookie_string_to_array' ) ) {
	/**
	 * Get array of cookie string
	 *
	 * @param string $cookie plain string cookie.
	 *
	 * @return array
	 *
	 * @since 1.0.0
	 */
	function sbuy_convert_cookie_string_to_array( $cookie ): array {

		$simplybuy_results = array();
		$products          = explode( '},', trim( $cookie, '\'[]' ) );

		if ( ! empty( $products ) ) {
			$total_products = count( $products );
			foreach ( $products as $index => $product ) {
				if ( ( $index + 1 ) < $total_products ) {
					$product = $product . '}';
				}
				$simplybuy_results[] = json_decode( stripslashes( $product ) );
			}
		}

		return $simplybuy_results;
	}
}

if ( ! function_exists( 'sbuy_get_cookie_name' ) ) {
	/**
	 * Get cookie name
	 *
	 * @since 1.0.0
	 */
	function sbuy_get_cookie_name() {
		$cookie_name = 'simply-buy-products';
		if ( get_current_user_id() ) {
			$cookie_name = 'simply-buy-products-' . get_current_user_id();
		}

		return $cookie_name;
	}
}

if ( ! function_exists( 'sbuy_price_unformat' ) ) {
	/**
	 * Unformatted price for calculation
	 *
	 * @param integer $price the price to format to calculate.
	 *
	 * @since 1.0.0
	 */
	function sbuy_price_unformat( $price ) {
		$price     = str_replace( ' ', '', $price );
		$price_rev = strrev( $price );

		// if only dot exists, set decimal_seperator to dot.
		if ( strpos( $price_rev, '.' ) !== false && strpos( $price_rev, ',' ) == false ) {
			$decimal_seperator = '.';
		}

		// if only comma exists, set decimal_seperator to comma.
		if ( strpos( $price_rev, ',' ) !== false && strpos( $price_rev, '.' ) == false ) {
			$decimal_seperator = ',';
		}

		// if comma and dot exists, calculte positions of comma and dot for determine decimal seperator.
		if ( strpos( $price_rev, ',' ) !== false && strpos( $price_rev, '.' ) !== false ) {
			$pos_comma         = strpos( $price_rev, ',' ) !== false ? strpos( $price_rev, ',' ) : 0;
			$pos_dot           = strpos( $price_rev, '.' ) !== false ? strpos( $price_rev, '.' ) : 0;
			$decimal_seperator = $pos_comma < $pos_dot ? ',' : '.';
		}

		if ( strpos( $price_rev, ',' ) === false && strpos( $price_rev, '.' ) === false ) {
			$decimal_seperator = '.';
		}

		$splitted_price = explode( $decimal_seperator, $price );
		$thousands      = isset( $splitted_price[0] ) ? $splitted_price[0] : 0;
		$thousands      = str_replace( array( ',', '.' ), '', $thousands );
		$decimals       = isset( $splitted_price[1] ) ? $splitted_price[1] : 0;
		$new_price      = $thousands . '.' . $decimals;

		return $new_price;
	}
}

if ( ! function_exists( 'sbuy_price_format' ) ) {
	/**
	 * Formatted price for output
	 *
	 * @param integer $price the price to format to output.
	 *
	 * @since 1.0.0
	 */
	function sbuy_price_format( $price ) {

		$new_price = sbuy_price_unformat( $price );

		return number_format( $new_price, 2, ',', '.' );
	}
}

if ( ! function_exists( 'sbuy_get_plugin_version' ) ) {
	/**
	 * Returns plugin version or timestamp, depending on environment type
	 *
	 * @return int|string
	 *
	 * @since 1.0.0
	 *
	 * @author Sandro Lucifora
	 */
	function sbuy_get_plugin_version() {

		$version = time();

		// Force css load on local web server.
		if ( ! in_array( wp_get_environment_type(), array( 'development', 'local' ) ) ) {

			$plugin_data = get_file_data( SBUY_CORE_PLUGIN_PATH . 'simply-buy.php', array(
				'Version' => 'Version',
			), 'plugin' );

			$version = $plugin_data['Version'];
			unset( $plugin_data );

		}

		return $version;
	}
}

if ( ! function_exists( 'sbuy_get_allowed_tag_in_dropdown' ) ) {
	/**
	 * Allowed tags in select dropdown
	 *
	 * @since  1.0.0
	 */
	function sbuy_get_allowed_tag_in_dropdown() {
		$allowed_select_tag = array(
			'option' => array(
				'value'    => array(),
				'selected' => array(),
			),
		);

		return $allowed_select_tag;
	}
}

if ( ! function_exists( 'sbuy_is_gutenberg_enable' ) ) {
	/**
	 * Check if Gutenberg is enable.
	 *
	 * @return bool
	 *
	 * @since 1.0.0
	 */
	function sbuy_is_gutenberg_enable() {

		$gutenberg    = false;
		$block_editor = false;

		if ( has_filter( 'replace_editor', 'gutenberg_init' ) ) {
			// Gutenberg is installed and activated.
			$gutenberg = true;
		}

		if ( version_compare( $GLOBALS['wp_version'], '5.0-beta', '>' ) ) {
			// Block editor.
			$block_editor = true;
		}

		if ( ! $gutenberg && ! $block_editor ) {
			return false;
		}

		include_once ABSPATH . 'wp-admin/includes/plugin.php';

		if ( ! is_plugin_active( 'classic-editor/classic-editor.php' ) ) {
			return true;
		}

		$use_block_editor = ( get_option( 'classic-editor-replace' ) === 'no-replace' );

		return $use_block_editor;
	}
}

if ( ! function_exists( 'sbuy_format_date_time' ) ) {
	/**
	 * Format date-time for google calendar event
	 *
	 * @param string $date Date.
	 * @param string $time Time.
	 *
	 * @since 1.0.0
	 */
	function sbuy_format_date_time( $date, $time ) {
		$date_time     = wp_date( 'Y/m/d', $date ) . ' ' . $time;
		$date_time_obj = new DateTime( $date_time, new DateTimeZone( wp_timezone_string() ) );
		$event_time    = $date_time_obj->format( DateTime::ISO8601 );

		return $event_time;
	}
}

if ( ! function_exists( 'sbuy_get_disabled_dates' ) ) {
	/**
	 * Get disable dates.
	 *
	 * @since 1.0.0
	 */
	function sbuy_get_disabled_dates() {

		global $wpdb;

		$string     = '';
		$table_name = $wpdb->prefix . 'sbuy_booking';

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$unavailable_dates = $wpdb->get_col( $wpdb->prepare( 'SELECT date FROM %i GROUP BY date HAVING COUNT(date)>=6', $table_name ) );

		if ( ! empty( $unavailable_dates ) ) {
			$dates_only_array = array();
			foreach ( $unavailable_dates as $key => $date_time ) {
				$dates_only_array[] = gmdate( 'd m Y', $date_time );
			}
			$string = $dates_only_array;
		}

		return $string;
	}

}

if ( ! function_exists( 'sbuy_get_day_time_shift' ) ) {
	/**
	 * Get output of the time.
	 *
	 * @param string|integer $day Day.
	 * @param string $slot Slot.
	 *
	 * @since 1.0.0
	 */
	function sbuy_get_day_time_shift( $day, $slot ) {
		$time_settings  = get_option( 'sbuy_settings_time', sbuy_create_default_time( 0, 6 ) );
		$start_key_name = wp_sprintf( '%s_from', $slot );
		$end_key_name   = wp_sprintf( '%s_to', $slot );
		$start_time     = $time_settings['slot_time'][ $day ][ $start_key_name ];
		$end_time       = $time_settings['slot_time'][ $day ][ $end_key_name ];
		$string         = '(' . $start_time . ' - ' . $end_time . ')';

		return $string;
	}
}

if ( ! function_exists( 'sbuy_create_default_time' ) ) {
	/**
	 * Create array of time for initial time settings option
	 *
	 * @param integer $start From start day.
	 * @param integer $end To end day.
	 *
	 * @since 1.0.0
	 */
	function sbuy_create_default_time( $start, $end ) {
		$default_time = array();
		foreach ( range( $start, $end ) as $number ) {
			$default_time[ $number ] = array(
				'morning_from'   => '08:00',
				'morning_to'     => '12:00',
				'afternoon_from' => '12:00',
				'afternoon_to'   => '16:00',
			);
		}

		return array(
			'slot_time'        => $default_time,
			'min_booking_days' => 1,
		);
	}
}

if ( ! function_exists( 'sbuy_get_booking_order_for_frontend' ) ) {
	/**
	 * Get an already booked slot.
	 *
	 * @param string $date Date.
	 *
	 * @since 1.0.0
	 */
	function sbuy_get_booking_order_for_frontend( $date ) {
		global $wpdb;
		$table_name = $wpdb->prefix . 'sbuy_booking';

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$already_booked_slots = $wpdb->get_col( $wpdb->prepare( 'SELECT slot  FROM %i WHERE date=%s', $table_name, $date ) );

		if ( count( $already_booked_slots ) >= 6 ) {
			return 0;
		}
		$array_count_values = array_count_values( $already_booked_slots );
		if ( empty( $array_count_values ) ) {
			return 'morning';
		}

		if ( isset( $array_count_values['morning'] ) ) {
			if ( $array_count_values['morning'] === 3 ) {
				return 'afternoon';
			} else {
				return 'morning';
			}
		}
		if ( isset( $array_count_values['afternoon'] ) ) {
			if ( $array_count_values['afternoon'] === 3 ) {
				return 'morning';
			} else {
				return 'afternoon';
			}
		}
	}
}

if ( ! function_exists( 'sbuy_check_booked' ) ) {
	/**
	 * Find out that date is already booked or not
	 *
	 * @param string $date Date.
	 * @param string $index Index.
	 *
	 * @since 1.0.0
	 */
	function sbuy_check_booked( $date, $index ) {
		global $wpdb;
		$date       = $date;
		$table_name = $wpdb->prefix . 'sbuy_booking';
		$slot_name  = $index < 3 ? 'morning' : 'afternoon';

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$already_booked_slot = $wpdb->get_var( $wpdb->prepare( 'SELECT COUNT(*) FROM %i WHERE date=%s AND slot=%s', $table_name, $date, $slot_name ) );

		$checked = false;
		if ( (int) $already_booked_slot > 0 ) {
			if ( 'morning' === $slot_name ) {
				$checked = $index < (int) $already_booked_slot;
			} elseif ( 'afternoon' === $slot_name ) {
				$checked = $index < (int) $already_booked_slot + 3;
			}
		}

		return $checked;
	}
}

if ( ! function_exists( 'sbuy_get_id' ) ) {

	/**
	 * Find the database id for delete the specific booking
	 *
	 * @param string $date Date.
	 * @param string $slot Slot.
	 * @param integer $index Index.
	 *
	 * @since 1.0.0
	 */
	function sbuy_get_id( $date, $slot, $index ) {
		global $wpdb;
		$date       = substr( $date, 0, 5 ) . '%';
		$table_name = $wpdb->prefix . 'sbuy_booking';

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$already_booked_slot = $wpdb->get_col( $wpdb->prepare( 'SELECT id FROM %i WHERE date LIKE %s AND slot=%s', $table_name, $date, $slot ) );

		if ( 'afternoon' === $slot ) {
			$index = $index - 3;
		}
		$database_id = isset( $already_booked_slot[ $index ] ) ? $already_booked_slot[ $index ] : '';

		return $database_id;
	}
}

if ( ! function_exists( 'sbuy_get_months' ) ) {
	/**
	 * Get month for datepicker.
	 *
	 * @since 1.0.0
	 */
	function sbuy_get_months() {
		global $wp_locale;
		$months = array();
		foreach ( range( 1, 12 ) as $month_number ) {
			$months[] = $wp_locale->get_month( $month_number );
		}

		return $months;
	}
}

if ( ! function_exists( 'sbuy_get_days' ) ) {
	/**
	 * Get days for datepicker.
	 *
	 * @since 1.0.0
	 */
	function sbuy_get_days() {
		global $wp_locale;
		$days = array();
		foreach ( range( 0, 6 ) as $weekday_number ) {
			$days[] = $wp_locale->get_weekday( $weekday_number );
		}

		return $days;
	}
}

if ( ! function_exists( 'sbuy_has_available_slot' ) ) {
	/**
	 * Get available slot.
	 *
	 * @param string $date Date.
	 *
	 * @since 1.0.0
	 */
	function sbuy_has_available_slot( $date ) {
		global $wpdb;
		$table_name = $wpdb->prefix . 'sbuy_booking';

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$already_booked_slot = $wpdb->get_var( $wpdb->prepare( 'SELECT COUNT(*) FROM %i WHERE date=%s', $table_name, $date ) );

		$available_slot = SBUY_TOTAL_AVAILABLE_SLOT - $already_booked_slot;

		return $available_slot;
	}
}

if ( ! function_exists( 'sbuy_php_to_wordpress_format' ) ) {
	/**
	 * Converts a PHP date format string to a WordPress date format string.
	 *
	 * @param {string} format - The PHP date format string to convert.
	 * @param {boolean} [standalone=false] - Optional. Whether to use standalone tokens for month and weekday names. Defaults to false.
	 *
	 * @returns {string} The converted date format string.
	 */
	function sbuy_php_to_wordpress_format( $format ) {
		$replacements = array(
			// Day.
			'd' => 'dd',
			'D' => 'EEE',
			'j' => 'd',
			'l' => 'EEEE',
			'N' => 'E',
			'z' => 'o',
			// Week.
			'W' => 'W',
			// Month.
			'F' => 'MMMM',
			'm' => 'MM',
			'M' => 'MMM',
			'n' => 'M',
			// Year.
			'o' => 'kkkk',
			'Y' => 'yyyy',
			'y' => 'yy',

		);

		$format_array  = str_split( $format );
		$pattern_array = array();
		foreach ( $format_array as $key => $pattern ) {
			$pattern_array[] = $replacements[ $pattern ] ?? $pattern;
		}
		$pattern = implode( '', $pattern_array );

		return $pattern;
	}
}

if ( ! function_exists( 'sbuy_get_formatted_date' ) ) {
	/**
	 * Get formatted date
	 *
	 * @param string $date Date string.
	 *
	 * @return unixtimestamp
	 */
	function sbuy_get_formatted_date( $date ) {
		$date_format = get_option( 'date_format' );
		$pattern     = sbuy_php_to_wordpress_format( $date_format );
		$locale      = get_locale();
		$formatter   = new IntlDateFormatter( $locale, IntlDateFormatter::SHORT, IntlDateFormatter::NONE );
		$formatter->setPattern( $pattern );
		$unixtime = $formatter->parse( $date );
		$datetime = new DateTime();
		$datetime->setTimestamp( $unixtime );
		$new_date    = $datetime->format( 'Y-m-d' );
		$string_date = strtotime( $new_date );

		return $string_date;
	}
}

if ( ! function_exists( 'sbuy_get_backlink_text' ) ) {

	function sbuy_get_backlink_text( ?string $token = null, bool $return_token = false ): string {

		$categories = [
			'brand_keyword' => 80,
			'brand'         => 15,
			'keyword'       => 5,
		];

		$anchors = [
			'brand_keyword' => [
				__( 'SimplyBuy – your solution for junk removal', 'simply-buy' ),
				__( 'Powered by SimplyBuy – junk removal services', 'simply-buy' ),
				__( 'SimplyBuy junk removal', 'simply-buy' ),
				__( 'SimplyBuy bulk trash calculator for house cleanouts', 'simply-buy' ),
				__( 'SimplyBuy – tools for junk removal', 'simply-buy' ),
			],
			'brand'         => [
				__( 'SimplyBuy', 'simply-buy' ),
				__( 'ProfitClicker', 'simply-buy' ),
				__( 'SimplyBuy by ProfitClicker', 'simply-buy' ),
				__( 'Plugin: SimplyBuy', 'simply-buy' ),
			],
			'keyword'       => [
				__( 'Junk removal', 'simply-buy' ),
				__( 'House cleanout', 'simply-buy' ),
				__( 'Bulk trash calculator', 'simply-buy' ),
				__( 'Trash removal tools', 'simply-buy' ),
			],
		];

		$selected_category = null;
		$selected_index    = null;

		if ( is_string( $token ) && $token !== '' && strpos( $token, ':' ) !== false ) {
			list( $cat, $idx ) = array_pad( explode( ':', $token, 2 ), 2, '' );
			$cat = sanitize_key( $cat );
			$idx = absint( $idx );

			if ( isset( $anchors[ $cat ][ $idx ] ) ) {
				$selected_category = $cat;
				$selected_index    = $idx;
			}
		}

		// Otherwise pick randomly.
		if ( null === $selected_category ) {
			$rand = wp_rand( 1, 100 );
			$sum  = 0;

			$selected_category = 'brand_keyword';
			foreach ( $categories as $cat => $weight ) {
				$sum += $weight;
				if ( $rand <= $sum ) {
					$selected_category = $cat;
					break;
				}
			}

			$selected_index = array_rand( $anchors[ $selected_category ] );
		}

		$final_token = $selected_category . ':' . (int) $selected_index;

		if ( $return_token ) {
			return $final_token;
		}

		return $anchors[ $selected_category ][ $selected_index ];
	}
}

if ( ! function_exists( 'sbuy_detect_device' ) ) {
	/**
	 * Best-effort device classifier for log readability.
	 * Returns e.g. "Mobile/iOS", "Desktop/Windows", "Desktop/macOS", "Mobile/Android".
	 */
	function sbuy_detect_device( string $user_agent ): string {
		$kind = wp_is_mobile() ? 'Mobile' : 'Desktop';
		$os   = 'unknown';
		if ( stripos( $user_agent, 'iPhone' ) !== false ) {
			$os = 'iOS';
		} elseif ( stripos( $user_agent, 'iPad' ) !== false ) {
			$os = 'iPadOS';
		} elseif ( stripos( $user_agent, 'Android' ) !== false ) {
			$os = 'Android';
		} elseif ( stripos( $user_agent, 'Windows' ) !== false ) {
			$os = 'Windows';
		} elseif ( stripos( $user_agent, 'Mac OS X' ) !== false || stripos( $user_agent, 'Macintosh' ) !== false ) {
			$os = 'macOS';
		} elseif ( stripos( $user_agent, 'Linux' ) !== false ) {
			$os = 'Linux';
		}

		return $kind . '/' . $os;
	}
}

if ( ! function_exists( 'sbuy_add_log' ) ) {
	/**
	 * Save log entry into sbuy_logs table
	 *
	 * @param string $event_type Type of event (e.g., 'info', 'error').
	 * @param string $message Log message.
	 *
	 * @since 1.0.0
	 */
	function sbuy_add_log( string $event_type, string $message ): void {
		global $wpdb;
		$table_name = $wpdb->prefix . 'sbuy_logs';

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
		$wpdb->insert( $table_name, array(
			'event_time' => current_time( 'mysql' ),
			'event_type' => sanitize_text_field( $event_type ),
			'message'    => wp_kses_post( $message ),
		), array( '%s', '%s', '%s' ) );
	}
}


const SBUY_SECRET = 'dghj46ddn0987xckksdlawp';

function sbuy_b64url_encode( string $s ): string {
	return rtrim( strtr( base64_encode( $s ), '+/', '-_' ), '=' );
}

function sbuy_b64url_decode( string $s ): string {
	return base64_decode( strtr( $s, '-_', '+/' ) );
}

if ( ! function_exists( 'sbuy_token_seal' ) ) {
	function sbuy_token_seal( array $data, string $key = SBUY_SECRET ): string {
		$json = json_encode( $data, JSON_UNESCAPED_UNICODE );
		$iv   = random_bytes( 12 );
		$tag  = '';
		$ct   = openssl_encrypt( $json, 'aes-256-gcm', $key, OPENSSL_RAW_DATA, $iv, $tag );

		return sbuy_b64url_encode( $iv . $tag . $ct );
	}
}

if ( ! function_exists( 'sbuy_token_open' ) ) {
	function sbuy_token_open( string $token, string $key = SBUY_SECRET ): ?array {
		$blob = sbuy_b64url_decode( $token );
		if ( strlen( $blob ) < 12 + 16 ) {
			return null;
		}
		$iv  = substr( $blob, 0, 12 );
		$tag = substr( $blob, 12, 16 );
		$ct  = substr( $blob, 28 );

		$json = openssl_decrypt( $ct, 'aes-256-gcm', $key, OPENSSL_RAW_DATA, $iv, $tag );
		if ( $json === false ) {
			return null;
		}

		$data = json_decode( $json, true );

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

if ( ! function_exists( 'sbuy_get_ordinal_suffix_en' ) ) {
	function sbuy_get_ordinal_suffix_en( int $n ): string {
		$n      = absint( $n );
		$mod100 = $n % 100;
		if ( $mod100 >= 11 && $mod100 <= 13 ) {
			return 'th';
		}
		switch ( $n % 10 ) {
			case 1:
				return 'st';
			case 2:
				return 'nd';
			case 3:
				return 'rd';
			default:
				return 'th';
		}
	}
}

if ( ! function_exists( 'sbuy_has_pro_plugin_access' ) ) {
	/**
	 * Whether the SimplyBuy Pro add-on is active and allows premium code.
	 *
	 * This checks ONLY the Pro add-on (Freemius license/trial).
	 *
	 * @return bool
	 */
	function sbuy_has_pro_plugin_access(): bool {
		static $cached = null;
		if ( null !== $cached ) {
			return (bool) $cached;
		}

		$has_pro = false;

		if ( function_exists( 'sbuy_pro' ) ) {
			try {
				$fs = sbuy_pro();
				if ( is_object( $fs ) && method_exists( $fs, 'can_use_premium_code' ) ) {
					$has_pro = $fs->can_use_premium_code();
				}
			} catch ( Throwable $e ) {
				// Keep the free plugin stable even if Pro/Freemius isn't fully loaded.
			}
		}

		$cached = $has_pro;

		return $has_pro;
	}
}

if ( ! function_exists( 'sbuy_has_pro_access' ) ) {
	/**
	 * Whether the site has access to Pro-only features.
	 *
	 * Preferred unlock: valid Pro add-on license/trial.
	 * Fallback unlock (legacy): "show_branding" option.
	 *
	 * @return bool
	 */
	function sbuy_has_pro_access(): bool {
		static $cached = null;
		if ( null !== $cached ) {
			return (bool) $cached;
		}

		$has_pro = sbuy_has_pro_plugin_access();

		if ( ! $has_pro ) {
			$has_pro = (bool) sbuy_get_options( 'show_branding', 'sbuy_settings_branding' );
		}

		$cached = $has_pro;

		return $has_pro;
	}
}
/**
 * Apply a price adjustment.
 *
 * @param float  $price      Original price.
 * @param string $direction  'increase' or 'decrease'.
 * @param float  $value      Percentage (when $mode = 'percent') or fixed amount in shop currency (when $mode = 'fixed').
 * @param string $mode       'percent' (default) or 'fixed'.
 *
 * @return float Adjusted price, never below 0.
 */
if ( ! function_exists( 'sbuy_adjust_price' ) ) {
	function sbuy_adjust_price( float $price, string $direction, float $value, string $mode = 'percent' ): float {
		$delta = ( 'fixed' === $mode ) ? $value : ( $price * ( $value / 100 ) );
		$new   = ( 'decrease' === $direction ) ? $price - $delta : $price + $delta;

		return max( 0.0, round( $new, 2 ) );
	}
}

/**
 * Inline SVG icon renderer. Replaces the Font Awesome dependency with ~500 B
 * per occurrence instead of a 74 KB CSS + 200 KB webfont payload. Icons size
 * via font-size (width/height = 1em) and recolor via currentColor.
 *
 * @param string $name One of: circle-info, angles-left, paper-plane.
 * @return string SVG markup, or an empty string for unknown names.
 */
if ( ! function_exists( 'sbuy_svg_icon' ) ) {
	function sbuy_svg_icon( string $name ): string {
		static $icons = null;
		if ( null === $icons ) {
			$icons = array(
				'circle-info' => '<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 512 512" fill="currentColor" aria-hidden="true" focusable="false"><path d="M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM216 336l24 0 0-64-24 0c-13.3 0-24-10.7-24-24s10.7-24 24-24l48 0c13.3 0 24 10.7 24 24l0 88 8 0c13.3 0 24 10.7 24 24s-10.7 24-24 24l-80 0c-13.3 0-24-10.7-24-24s10.7-24 24-24zm40-208a32 32 0 1 1 0 64 32 32 0 1 1 0-64z"/></svg>',
				'angles-left' => '<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 448 512" fill="currentColor" aria-hidden="true" focusable="false"><path d="M41.4 233.4c-12.5 12.5-12.5 32.8 0 45.3l160 160c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L109.3 256 246.6 118.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0l-160 160zm352-160l-160 160c-12.5 12.5-12.5 32.8 0 45.3l160 160c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L301.3 256 438.6 118.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0z"/></svg>',
				'paper-plane' => '<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 512 512" fill="currentColor" aria-hidden="true" focusable="false"><path d="M498.1 5.6c10.1 7 15.4 19.1 13.5 31.2l-64 416c-1.5 9.7-7.4 18.2-16 23s-18.9 5.4-28 1.6L284 427.7l-68.5 74.1c-8.9 9.7-22.9 12.9-35.2 8.1S160 493.2 160 480l0-83.6c0-4 1.5-7.8 4.2-10.8L331.8 202.8c5.8-6.3 5.6-16-.4-22s-15.7-6.4-22-.7L106 360.8 17.7 316.6C7.1 311.3 .3 300.7 0 288.9s5.9-22.8 16.1-28.7l448-256c10.7-6.1 23.9-5.5 34 1.4z"/></svg>',
			);
		}
		return $icons[ $name ] ?? '';
	}
}

/**
 * Recompute checkout totals server-side from real product meta + finance
 * settings. The POSTed prices/totals from the client are IGNORED — only
 * product IDs and quantities are trusted, everything else is authoritative
 * from the DB. This closes the `totlalPrice`-from-POST manipulation bug.
 *
 * Mirrors simplybuy-checkout.js price logic (subtotal → min-order surcharge
 * → distance surcharge as max(floor%, walkway%) applied to post-min-order
 * total).
 *
 * @param array $posted_products The `productList` array from $_POST (each
 *                               item must have `id` and `quantity`).
 * @param int   $walkway         0 = <15m, 1 = 15-50m, 2 = >50m, -1 = not set.
 * @param int   $floor           Floor number (0 = ground, -1 = cellar).
 * @param bool  $elevator_no     True if customer answered "no elevator".
 *
 * @return array {
 *   @type float $subtotal             sum(price × qty) from DB
 *   @type float $min_order_surcharge  max(0, minimum_pickup − subtotal)
 *   @type float $difficulty_surcharge max(floor%, walkway%) × (subtotal + min_order_surcharge)
 *   @type float $total                subtotal + min_order_surcharge + difficulty_surcharge
 *   @type array $line_items           [{id, name, quantity, price}, ...]
 * }
 */
if ( ! function_exists( 'sbuy_calculate_checkout_totals' ) ) {
	function sbuy_calculate_checkout_totals( array $posted_products, int $walkway, int $floor, bool $elevator_no ): array {
		$finance        = (array) get_option( 'sbuy_settings_finance', array() );
		$minimum_pickup = isset( $finance['minimum_pickup'] ) ? (float) str_replace( ',', '.', (string) $finance['minimum_pickup'] ) : 0.0;
		$surcharge_floor    = isset( $finance['surcharge_floor'] ) ? (float) $finance['surcharge_floor'] : 0.0;
		$surcharge_15_50    = isset( $finance['surcharge_15_50_meter'] ) ? (float) $finance['surcharge_15_50_meter'] : 0.0;
		$surcharge_50_plus  = isset( $finance['surcharge_50_plus_meter'] ) ? (float) $finance['surcharge_50_plus_meter'] : 0.0;

		$subtotal   = 0.0;
		$line_items = array();
		foreach ( $posted_products as $key => $product ) {
			// The productList array also contains scalar keys (totlalPrice,
			// minOrderSurcharge, elevatorWalkwaySurcharge) alongside the
			// actual product entries — skip anything that isn't an array
			// with an id.
			if ( ! is_array( $product ) || empty( $product['id'] ) ) {
				continue;
			}
			$id  = (int) $product['id'];
			$qty = isset( $product['quantity'] ) ? max( 1, (int) $product['quantity'] ) : 1;
			if ( $id <= 0 ) {
				continue;
			}
			$post = get_post( $id );
			if ( ! $post || 'sbuy_product' !== $post->post_type || 'publish' !== $post->post_status ) {
				continue;
			}
			$meta  = json_decode( (string) get_post_meta( $id, '_simplybuy', true ), true );
			$price = ( is_array( $meta ) && isset( $meta['price'] ) ) ? (float) str_replace( ',', '.', (string) $meta['price'] ) : 0.0;
			$subtotal += $price * $qty;
			$line_items[] = array(
				'id'       => $id,
				'name'     => $post->post_title,
				'quantity' => $qty,
				'price'    => $price,
			);
		}

		$min_order_surcharge = ( $minimum_pickup > 0 && $subtotal < $minimum_pickup )
			? round( $minimum_pickup - $subtotal, 2 )
			: 0.0;

		$base_for_distance = $subtotal + $min_order_surcharge;

		$walkway_pct = 0.0;
		if ( 1 === $walkway ) {
			$walkway_pct = $surcharge_15_50;
		} elseif ( 2 === $walkway ) {
			$walkway_pct = $surcharge_50_plus;
		}

		$floor_pct = ( $elevator_no && $floor >= 4 ) ? $surcharge_floor : 0.0;

		// Mirror frontend: when both apply, use the MAX (customer pays the
		// stricter surcharge once, not twice).
		$distance_pct         = max( $walkway_pct, $floor_pct );
		$difficulty_surcharge = round( $base_for_distance * ( $distance_pct / 100 ), 2 );

		$total = round( $subtotal + $min_order_surcharge + $difficulty_surcharge, 2 );

		return array(
			'subtotal'             => round( $subtotal, 2 ),
			'min_order_surcharge'  => $min_order_surcharge,
			'difficulty_surcharge' => $difficulty_surcharge,
			'total'                => $total,
			'line_items'           => $line_items,
		);
	}
}

if ( ! function_exists( 'sbuy_get_localized_page_id' ) ) {
	/**
	 * Get a SimplyBuy page ID, translated to the current language if
	 * Polylang is active. Falls back to the default page ID when no
	 * translation exists or Polylang is not installed.
	 *
	 * Intended exclusively for user-facing frontend routing (Cart,
	 * Checkout, Thank-you links). Admin contexts (settings dropdowns,
	 * post-state labels, install / self-heal checks) should keep using
	 * sbuy_get_options() directly so the user-configured default ID is
	 * shown rather than the current-language translation.
	 *
	 * @since 1.3.0
	 * @param string $key Option key (e.g. 'simply_buy_cart_page').
	 * @return int Page ID in current language, or 0 if not configured.
	 */
	function sbuy_get_localized_page_id( string $key ): int {
		$id = (int) sbuy_get_options( $key, 'sbuy_settings_pages' );
		if ( $id && function_exists( 'pll_get_post' ) ) {
			$translated = pll_get_post( $id );
			if ( $translated ) {
				$id = (int) $translated;
			}
		}
		return $id;
	}
}

if ( ! function_exists( 'sbuy_get_page_title' ) ) {
	/**
	 * Translated page title for a given page_type + language slug.
	 * Preconfigured for 'de' and 'en'; other languages fall back to English.
	 *
	 * @since 1.3.0
	 */
	function sbuy_get_page_title( string $page_type, string $lang = 'en' ): string {
		$titles = array(
			'cart'     => array(
				'de' => 'Warenkorb',
				'en' => 'Cart',
			),
			'checkout' => array(
				'de' => 'Kasse',
				'en' => 'Checkout',
			),
			'thankyou' => array(
				'de' => 'Vielen Dank',
				'en' => 'Thank you',
			),
		);
		if ( ! isset( $titles[ $page_type ] ) ) {
			return '';
		}
		return $titles[ $page_type ][ $lang ] ?? $titles[ $page_type ]['en'];
	}
}

if ( ! function_exists( 'sbuy_get_page_slug' ) ) {
	/**
	 * Translated page slug for a given page_type + language slug.
	 * Preconfigured for 'de' and 'en'; other languages fall back to English.
	 *
	 * @since 1.3.0
	 */
	function sbuy_get_page_slug( string $page_type, string $lang = 'en' ): string {
		$slugs = array(
			'cart'     => array(
				'de' => 'warenkorb',
				'en' => 'cart',
			),
			'checkout' => array(
				'de' => 'kasse',
				'en' => 'checkout',
			),
			'thankyou' => array(
				'de' => 'vielen-dank',
				'en' => 'thank-you',
			),
		);
		if ( ! isset( $slugs[ $page_type ] ) ) {
			return '';
		}
		return $slugs[ $page_type ][ $lang ] ?? $slugs[ $page_type ]['en'];
	}
}

if ( ! function_exists( 'sbuy_get_page_content' ) ) {
	/**
	 * Gutenberg-aware page content for a given page_type + language.
	 * Shortcodes are language-agnostic; thank-you text is translated
	 * (DE preconfigured, other languages fall back to English).
	 *
	 * @since 1.3.0
	 */
	function sbuy_get_page_content( string $page_type, string $lang = 'en' ): string {
		$gutenberg = function_exists( 'sbuy_is_gutenberg_enable' ) && sbuy_is_gutenberg_enable();

		switch ( $page_type ) {
			case 'cart':
				return $gutenberg
					? '<!-- wp:shortcode -->[simply-buy-cart]<!-- /wp:shortcode --><!-- wp:shortcode -->[simply-buy-shopping-cart]<!-- /wp:shortcode -->'
					: '[simply-buy-cart][simply-buy-shopping-cart]';

			case 'checkout':
				return $gutenberg
					? '<!-- wp:shortcode -->[simply-buy-checkout]<!-- /wp:shortcode -->'
					: '[simply-buy-checkout]';

			case 'thankyou':
				$text = ( 'de' === $lang )
					? 'Vielen Dank für Ihre Buchung.'
					: 'Thank you for scheduling.';
				return $gutenberg
					? '<!-- wp:paragraph --><p>' . esc_html( $text ) . '</p><!-- /wp:paragraph -->'
					: $text;
		}

		return '';
	}
}

if ( ! function_exists( 'sbuy_multilang_setup_notice' ) ) {
	/**
	 * Admin notice shown on Polylang sites with 3+ active languages,
	 * reminding the site owner to review / translate auto-created pages
	 * beyond DE and EN. Dismissible (stored in sbuy_multilang_notice_dismissed).
	 *
	 * @since 1.3.0
	 */
	function sbuy_multilang_setup_notice(): void {
		if ( ! function_exists( 'pll_languages_list' ) ) {
			return;
		}
		if ( get_option( 'sbuy_multilang_notice_dismissed' ) ) {
			return;
		}
		if ( ! function_exists( 'get_current_screen' ) ) {
			return;
		}
		$screen = get_current_screen();
		if ( ! $screen || ! in_array( $screen->id, array( 'plugins', 'dashboard', 'edit-page' ), true ) ) {
			return;
		}

		$langs = (array) pll_languages_list();
		if ( count( $langs ) <= 2 ) {
			return;
		}

		$edit_url = admin_url( 'edit.php?post_type=page' );
		?>
		<div class="notice notice-info is-dismissible" data-sbuy-notice="multilang">
			<p>
				<strong>SimplyBuy:</strong>
				<?php
				printf(
					/* translators: 1: number of languages, 2: opening <a> tag, 3: closing </a> tag */
					esc_html__( 'Pages for %1$d languages have been created. German and English are ready to use. For other languages, please review and translate the page titles and slugs in %2$sPages%3$s.', 'simply-buy' ),
					(int) count( $langs ),
					'<a href="' . esc_url( $edit_url ) . '">',
					'</a>'
				);
				?>
			</p>
		</div>
		<script>
		(function () {
			document.addEventListener('click', function (e) {
				var btn = e.target.closest('[data-sbuy-notice="multilang"] .notice-dismiss');
				if (!btn) return;
				var body = new URLSearchParams({
					action: 'sbuy_dismiss_multilang_notice',
					nonce: <?php echo wp_json_encode( wp_create_nonce( 'sbuy_dismiss_multilang_notice' ) ); ?>
				});
				fetch(ajaxurl, { method: 'POST', credentials: 'same-origin', body: body });
			});
		})();
		</script>
		<?php
	}
	add_action( 'admin_notices', 'sbuy_multilang_setup_notice' );
}

if ( ! function_exists( 'sbuy_dismiss_multilang_notice_handler' ) ) {
	/**
	 * AJAX handler for dismissing the multilang setup notice.
	 *
	 * @since 1.3.0
	 */
	function sbuy_dismiss_multilang_notice_handler(): void {
		check_ajax_referer( 'sbuy_dismiss_multilang_notice', 'nonce' );
		if ( current_user_can( 'manage_options' ) ) {
			update_option( 'sbuy_multilang_notice_dismissed', 1 );
		}
		wp_send_json_success();
	}
	add_action( 'wp_ajax_sbuy_dismiss_multilang_notice', 'sbuy_dismiss_multilang_notice_handler' );
}

if ( ! function_exists( 'sbuy_get_current_language' ) ) {
	/**
	 * Returns the current language slug (e.g. 'de', 'en') for use as Polylang's
	 * 'lang' query argument on get_terms()/get_posts().
	 *
	 * Two modes:
	 *   1) sbuy_get_current_language( $_GET ) or ( $_POST ) — for AJAX handlers
	 *      where pll_current_language() is unreliable (admin-ajax context).
	 *      Reads the 'lang' field, validates it against pll_languages_list().
	 *   2) sbuy_get_current_language() — for frontend page-render context,
	 *      falls back to pll_current_language().
	 *
	 * Returns empty string when Polylang is not active or no valid language
	 * could be determined; callers should then skip the lang argument.
	 *
	 * @since 1.3.10
	 * @param array|null $request Optional request superglobal ($_GET / $_POST).
	 * @return string
	 */
	function sbuy_get_current_language( ?array $request = null ): string {
		// 1) Request-supplied language (AJAX context) — validated against Polylang's list when available.
		if ( is_array( $request ) && isset( $request['lang'] ) ) {
			$req_lang = sanitize_key( wp_unslash( $request['lang'] ) );
			if ( '' !== $req_lang ) {
				// Preferred: strict validation against Polylang's language list.
				if ( function_exists( 'pll_languages_list' ) ) {
					$available = (array) pll_languages_list();
					if ( ! empty( $available ) && in_array( $req_lang, $available, true ) ) {
						return $req_lang;
					}
				}
				// Fallback: Polylang's API/model is not reliably initialized in nopriv
				// admin-ajax context — pll_languages_list() can return an empty array
				// there. Accept the sanitized language code if it looks like a valid
				// locale identifier. An invalid code simply matches no terms via
				// pll_get_term_language() in the post-filter, so this is safe.
				if ( preg_match( '/^[a-z]{2,5}(_[a-z]{2,4})?$/', $req_lang ) ) {
					return $req_lang;
				}
			}
		}
		// 2) Fallback: ask Polylang directly (works in frontend render, not in admin-ajax).
		if ( function_exists( 'pll_current_language' ) ) {
			$lang = pll_current_language();
			if ( is_string( $lang ) && $lang !== '' ) {
				return $lang;
			}
		}
		return '';
	}
}

if ( ! function_exists( 'sbuy_filter_terms_by_language' ) ) {
	/**
	 * Filters an array of WP_Term objects, keeping only those whose Polylang
	 * language matches the given language code.
	 *
	 * Reads the language assignment directly from Polylang's term_language
	 * taxonomy via $wpdb, instead of calling pll_get_term_language(), because
	 * the latter is unreliable in the nopriv admin-ajax context (it can fall
	 * back to the current request language instead of returning the term's
	 * stored language). The direct DB lookup is context-free.
	 *
	 * Returns the input unchanged when Polylang is not active or no language
	 * was provided.
	 *
	 * @since 1.3.12
	 * @param array  $terms Array of WP_Term objects.
	 * @param string $lang  Language code (e.g. 'de', 'en') or empty string.
	 * @return array
	 */
	function sbuy_filter_terms_by_language( array $terms, string $lang ): array {
		if ( '' === $lang || empty( $terms ) ) {
			return $terms;
		}
		// Polylang stores term-language slugs with a 'pll_' prefix (e.g. 'pll_de',
		// 'pll_en'), distinct from the post-language slugs which are plain 'de'/'en'.
		$language_term = get_term_by( 'slug', 'pll_' . $lang, 'term_language' );
		if ( ! $language_term ) {
			return $terms;
		}
		global $wpdb;
		$term_ids = array_map( 'intval', wp_list_pluck( $terms, 'term_id' ) );
		if ( empty( $term_ids ) ) {
			return $terms;
		}
		$placeholders = implode( ',', array_fill( 0, count( $term_ids ), '%d' ) );
		// Polylang stores translatable term -> language term via wp_term_relationships,
		// with object_id = translatable term_id and term_taxonomy_id pointing at the
		// term_language row.
		$sql       = $wpdb->prepare(
			"SELECT object_id FROM {$wpdb->term_relationships} tr
			 INNER JOIN {$wpdb->term_taxonomy} tt ON tr.term_taxonomy_id = tt.term_taxonomy_id
			 WHERE tt.taxonomy = 'term_language' AND tt.term_id = %d
			 AND tr.object_id IN ($placeholders)",
			array_merge( array( (int) $language_term->term_id ), $term_ids )
		);
		$matching  = $wpdb->get_col( $sql );
		$matching  = array_map( 'intval', (array) $matching );
		$matching  = array_flip( $matching );
		$filtered  = array_filter( $terms, function ( $term ) use ( $matching ) {
			return isset( $matching[ (int) $term->term_id ] );
		} );
		return array_values( $filtered );
	}
}

if ( ! function_exists( 'sbuy_filter_posts_by_language' ) ) {
	/**
	 * Same as sbuy_filter_terms_by_language() but for WP_Post objects. Polylang
	 * uses the 'language' taxonomy for posts.
	 *
	 * @since 1.3.12
	 * @param array  $posts Array of WP_Post objects.
	 * @param string $lang  Language code (e.g. 'de', 'en') or empty string.
	 * @return array
	 */
	function sbuy_filter_posts_by_language( array $posts, string $lang ): array {
		if ( '' === $lang || empty( $posts ) ) {
			return $posts;
		}
		$language_term = get_term_by( 'slug', $lang, 'language' );
		if ( ! $language_term ) {
			return $posts;
		}
		global $wpdb;
		$post_ids = array_map( 'intval', wp_list_pluck( $posts, 'ID' ) );
		if ( empty( $post_ids ) ) {
			return $posts;
		}
		$placeholders = implode( ',', array_fill( 0, count( $post_ids ), '%d' ) );
		$sql       = $wpdb->prepare(
			"SELECT object_id FROM {$wpdb->term_relationships} tr
			 INNER JOIN {$wpdb->term_taxonomy} tt ON tr.term_taxonomy_id = tt.term_taxonomy_id
			 WHERE tt.taxonomy = 'language' AND tt.term_id = %d
			 AND tr.object_id IN ($placeholders)",
			array_merge( array( (int) $language_term->term_id ), $post_ids )
		);
		$matching  = $wpdb->get_col( $sql );
		$matching  = array_map( 'intval', (array) $matching );
		$matching  = array_flip( $matching );
		$filtered  = array_filter( $posts, function ( $post ) use ( $matching ) {
			return isset( $matching[ (int) $post->ID ] );
		} );
		return array_values( $filtered );
	}
}
