<?php

/**
 * @package WP Product Feed Manager/Data/Functions
 * @version 2.7.0
 */

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

/**
 * Converts a string containing a date-time stamp as stored in the metadata to a date time string
 * that can be used in a feed file
 *
 * @param string $date_stamp The timestamp that needs to be converted to a string that can be stored in a feed file
 *
 * @return string    A string containing the time or an empty string if the $date_stamp is empty
 * @since 1.1.0
 *
 */
function wppfm_convert_price_date_to_feed_format( $date_stamp ) {
	if ( $date_stamp ) {
		return gmdate( 'Y-m-d\TH:iO', $date_stamp );
	} else {
		return '';
	}
}

/**
 * After a channel has been updated, this function decreases the 'wppfm_channels_to_update' option with one
 *
 * @since 1.4.1
 */
function wppfm_decrease_update_ready_channels() {
	$old = get_option( 'wppfm_channels_to_update' );

	if ( $old > 0 ) {
		update_option( 'wppfm_channels_to_update', $old - 1 );
	} else {
		update_option( 'wppfm_channels_to_update', 0 );
	}
}

/**
 * Checks the current database version and updates it if required
 *
 * @since 2.4.0
 */
function wppfm_check_db_version() {
	$db_management = new WPPFM_Database_Management();
	$db_management->verify_db_version();
}

/**
 * Checks if a specific source key is money related key or not
 *
 * @param string $key The source key to be checked
 *
 * @return boolean    True if the source key is money-related, false if not
 * @since 1.1.0
 *
 */
function wppfm_meta_key_is_money( $key ) {
	// money keys
	$special_price_keys = array(
		'_max_variation_price',
		'_max_variation_regular_price',
		'_max_variation_sale_price',
		'_min_variation_price',
		'_min_variation_regular_price',
		'_min_variation_sale_price',
		'_regular_price',
		'_regular_price_with_tax',
		'_regular_price_without_tax',
		'_sale_price',
		'_sale_price_with_tax',
		'_sale_price_without_tax',
		'_max_group_price',
		'_min_group_price',
		'regular_price',
		'sale_price',
	);

	return in_array( $key, $special_price_keys, true );
}

/**
 * Takes a value and formats it to a money value using the WooCommerce thousands separator, decimal separator and number of decimal values
 *
 * @param string $money_value The money values to be formatted
 * @param string $feed_language Selected Language in WPML add-on, leave empty if no exchange rate correction is required @since 1.9.0
 * @param string $feed_currency Selected currency in WOOCS add-on, leave empty if no correction is required @since 2.28.0.
 *
 * @return string    A formatted money value
 * @since 1.9.0 added WPML support
 * @since 2.28.0 Switched to the formal wc functions to get the separator and number of decimal values.
 * @since 2.28.0 Added support for the WooCommerce Currency Switcher plugin.
 * @since 2.31.0 Added the wppfm_feed_price_thousands_separator, wppfm_feed_price_decimal_separator and wppfm_feed_price_decimals filters.
 * @since 2.36.1 Return an empty string if the $money_value parameter is an empty string. This is required to allow filtering feed attributes on "is empty" parameters.
 *
 * @since 1.1.0
 */
function wppfm_prep_money_values( $money_value, $feed_language = '', $feed_currency = '' ) {

	if ( '' === $money_value ) {
		return $money_value;
	}

	$thousand_separator = apply_filters( 'wppfm_feed_price_thousands_separator', wc_get_price_thousand_separator() );

	if ( ! is_float( $money_value ) ) {
		$val         = wppfm_number_format_parse( $money_value );
		$money_value = floatval( $val );
	}

	if ( has_filter( 'wppfm_woocs_exchange_money_values' ) ) { // WOOCS Support.
		$money_value = apply_filters( 'wppfm_woocs_exchange_money_values', $money_value, $feed_currency );
	}

	if ( has_filter( 'wppfm_wpml_exchange_money_values' ) ) { // WPML Support.
		return apply_filters( 'wppfm_wpml_exchange_money_values', $money_value, $feed_language );
	} else {
		$decimal_point   = apply_filters( 'wppfm_feed_price_decimal_separator', wc_get_price_decimal_separator() );
		$number_decimals = apply_filters( 'wppfm_feed_price_decimals', wc_get_price_decimals() );

		// To prevent Google Merchant Centre to interpret a thousand separator as a decimal separator, we need to remove
		// the thousand separators if the decimals setting in WC is 0 and a period is used as a decimal separator.
		// E.g., 1.452 would be interpreted by Google as 1,452.
		// @since 2.11.0
		if ( 0 === $number_decimals && '.' === $thousand_separator ) {
			$thousand_separator = '';
		}

		return number_format( $money_value, $number_decimals, $decimal_point, $thousand_separator );
	}
}

/**
 * Checks if there are invalid backups
 *
 * @return boolean true, if there are no backups or these backups are current
 * @since 1.8.0
 *
 */
function wppfm_check_backup_status() {
	if ( ! WPPFM_Db_Management::invalid_backup_exist() ) {
		return true;
	} else {
		return false;
	}
}

/**
 * Checks a folder given by $path for SQL files and returns their names including the path
 *
 * @param $path
 * @since 2.6.0
 *
 * @return array
 */
function wppfm_list_sql_files( $path ) {
	$files = array();

	if ( is_dir( $path ) ) {
		$handle = opendir( $path );
		if ( $handle ) {
			while ( false !== ( $name = readdir( $handle ) ) ) { // phpcs:ignore WordPress.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition
				if ( preg_match( '/[a-zA-Z0-9-_ ]{2,}[.](sql)$/', $name ) ) {
					$files[] = $path . '/' . $name;
				}
			}
		}
	}

	return $files;
}

/**
 * Forces the database to load and update and adds the auto update cron event if it does not exist
 *
 * @since 1.9.0
 * @return boolean
 */
function wppfm_reinitiate_plugin() {
	wppfm_check_feed_update_schedule();
	wppfm_clear_feed_process_data();

	// remakes the database
	$db = new WPPFM_Database_Management();
	$db->force_reinitiate_db();

	$plugin_prefixes = apply_filters( 'wppfm_edd_plugin_prefix_list', array( 'wppfm' ) );

	// resets the license nr
	foreach ( $plugin_prefixes as $plugin_prefix ) {
		delete_option( $plugin_prefix . '_lic_status' );
		delete_option( $plugin_prefix . '_lic_status_date' );
		delete_option( $plugin_prefix . '_lic_key' );
		delete_option( $plugin_prefix . '_lic_expires' );
		delete_option( $plugin_prefix . '_license_notice_suppressed' );
	}

	// reset the keyed options
	WPPFM_Db_Management::clean_options_table();

	do_action( 'wppfm_plugin_reinitialized' );

	return true;
}

/**
 * Returns the global WP_Filesystem object.
 *
 * @since 3.12.0
 * @since 3.14.0 Added the wppfm_initialize_wp_filesystem function to initialize the WP_Filesystem object even if the.
 * @return object WP_Filesystem
 */
function wppfm_get_wp_filesystem() {
	global $wp_filesystem;

	if ( ! $wp_filesystem instanceof WP_Filesystem_Base ) {
		$initialized = wppfm_initialize_wp_filesystem();

		if ( false === $initialized ) {
			die( esc_html__( 'WP_Filesystem could not be initialized', 'wp-product-feed-manager' ) );
		}
	}

	if ( WP_Filesystem() ) {
		return $wp_filesystem;
	} else {
		$credentials = request_filesystem_credentials( '', 'ftp' );

		if ( $credentials && WP_Filesystem( $credentials ) ) {
			return $wp_filesystem;
		} else {
			die( esc_html__( 'Unable to initialize WP_Filesystem!', 'wp-product-feed-manager' ) );
		}
	}
}

/**
 * Initializes the WP_Filesystem object.
 *
 * @since 3.14.0
 * @return bool True if the WP_Filesystem object was successfully initialized, false if not.
 */
function wppfm_initialize_wp_filesystem() {
	global $wp_filesystem;

	if ( $wp_filesystem instanceof WP_Filesystem_Base ) {
		return true;
	}

	require_once ABSPATH . 'wp-admin/includes/file.php';

	$method      = wppfm_get_wp_filesystem_method_or_direct();
	$initialized = false;

	if ( 'direct' === $method ) {
		$initialized = WP_Filesystem();
	} else {
		ob_start();
		$credentials = request_filesystem_credentials( '' );
		ob_end_clean();

		$initialized = $credentials && WP_Filesystem( $credentials );
	}

	return is_null( $initialized ) ? false : $initialized;

}

/**
 * Returns the method that the WP_Filesystem object should be initialized with.
 *
 * @since 3.14.0
 * @return string The method that the WP_Filesystem object should be initialized with.
 */
function wppfm_get_wp_filesystem_method_or_direct() {
	$method = 'direct';

	if ( defined( 'FS_METHOD' ) && 'direct' !== FS_METHOD ) {
		$method = FS_METHOD;
	}

	return $method;
}

/**
 * Adds a new line to the end of a file.
 * Uses native PHP file operations with append mode and file locking to prevent race conditions.
 * Falls back to WP_Filesystem for FTP/SSH scenarios.
 *
 * @param string $file_path The path to the file.
 * @param string $new_line  The new line to be added.
 * @param bool $add_crt     Add a carriage return at the end of the line. Default is false.
 *
 * @since 3.12.0
 * @since 3.15.0 - Improved to use true append mode with file locking to prevent race conditions and corruption.
 * @return bool|int The number of bytes written, or false on failure.
 */
function wppfm_append_line_to_file( $file_path, $new_line, $add_crt = false ) {
	// Prepare the line to write
	$line_to_write = $new_line . ( $add_crt ? PHP_EOL : '' );

	// Track file operation for performance monitoring
	do_action( 'wppfm_before_file_write', $line_to_write );

	// Try to use native PHP file operations first (most efficient and safe for local files)
	// This prevents race conditions by using append mode with file locking
	$real_path = wppfm_get_real_file_path( $file_path );

	// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_is_writable -- Prefer native checks for direct FS access before WP_Filesystem fallback.
	if ( $real_path && is_writable( dirname( $real_path ) ) ) {
		// Use native PHP file operations with append mode and locking
		// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen -- Native append is faster when direct FS access is available.
		$handle = @fopen( $real_path, 'a' );

		if ( false !== $handle ) {
			// Try to acquire exclusive lock to prevent race conditions
			// LOCK_EX = exclusive lock (write lock), blocks other processes
			// First try non-blocking lock (fast path)
			$lock_acquired = @flock( $handle, LOCK_EX | LOCK_NB );

			// If non-blocking lock failed, try blocking lock with retries
			// This ensures data integrity even under high contention
			if ( ! $lock_acquired ) {
				$max_retries = 3;
				$retry_delay = 100000; // 100ms in microseconds
				$retry_count = 0;

				while ( $retry_count < $max_retries && ! $lock_acquired ) {
					usleep( $retry_delay );
					$lock_acquired = @flock( $handle, LOCK_EX | LOCK_NB );
					$retry_count++;
				}

				// If still no lock after retries, try blocking lock (will wait)
				// This is safe because append operations are very fast
				if ( ! $lock_acquired ) {
					$lock_acquired = @flock( $handle, LOCK_EX );
				}
			}

			if ( $lock_acquired ) {
				// Write the line
				// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fwrite -- Native write keeps append atomic before WP_Filesystem fallback.
				$bytes_written = @fwrite( $handle, $line_to_write );

				// Release lock immediately after write
				@flock( $handle, LOCK_UN );
				// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- Close native handle used for direct writes.
				@fclose( $handle );

				if ( false !== $bytes_written ) {
					return $bytes_written;
				}
			} else {
				// Could not acquire lock after all attempts, close handle and fall through to fallback
				// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- Close native handle before WP_Filesystem fallback.
				@fclose( $handle );
			}
		}
	}

	// Fallback to WP_Filesystem method (for FTP/SSH or if native operations fail)
	// This maintains compatibility but is less efficient for large files
	$wp_filesystem = wppfm_get_wp_filesystem();

	// For fallback, we still need to read-all-write-all, but this should be rare
	$contents  = $wp_filesystem->exists( $file_path ) ? $wp_filesystem->get_contents( $file_path ) : '';
	$contents .= $line_to_write;

	return $wp_filesystem->put_contents( $file_path, $contents, FS_CHMOD_FILE );
}

/**
 * Gets the real file system path for a file.
 * Handles both absolute and relative paths, and resolves WP_Filesystem paths when possible.
 *
 * @param string $file_path The file path (may be absolute or relative).
 *
 * @since 3.15.0
 * @return string|false The real file system path, or false if cannot be determined.
 */
function wppfm_get_real_file_path( $file_path ) {
	if ( empty( $file_path ) ) {
		return false;
	}

	// Normalize the path (remove any trailing slashes, resolve . and ..)
	$file_path = rtrim( $file_path, '/' );

	// If file exists, get its realpath
	if ( file_exists( $file_path ) && is_file( $file_path ) ) {
		$real_path = realpath( $file_path );
		if ( $real_path ) {
			return $real_path;
		}
	}

	// If file doesn't exist yet (new file), try to resolve the directory
	$dir_path = dirname( $file_path );
	$file_name = basename( $file_path );

	// Check if directory exists and is writable
	// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_is_writable -- Direct path writable checks avoid unnecessary filesystem init.
	if ( file_exists( $dir_path ) && is_dir( $dir_path ) && is_writable( $dir_path ) ) {
		$real_dir = realpath( $dir_path );
		if ( $real_dir ) {
			return $real_dir . '/' . $file_name;
		}
	}

	// Try to resolve using WPPFM_FEEDS_DIR if defined
	if ( defined( 'WPPFM_FEEDS_DIR' ) ) {
		// Check if the path contains WPPFM_FEEDS_DIR
		if ( strpos( $file_path, WPPFM_FEEDS_DIR ) === 0 ) {
			// Path already contains WPPFM_FEEDS_DIR, try to resolve it
			if ( file_exists( WPPFM_FEEDS_DIR ) && is_dir( WPPFM_FEEDS_DIR ) ) {
				$real_feeds_dir = realpath( WPPFM_FEEDS_DIR );
				if ( $real_feeds_dir ) {
					// Extract relative path from WPPFM_FEEDS_DIR
					$relative_path = substr( $file_path, strlen( WPPFM_FEEDS_DIR ) );
					$relative_path = ltrim( $relative_path, '/' );
					return $real_feeds_dir . '/' . $relative_path;
				}
			}
		}

		// Try to find file in WPPFM_FEEDS_DIR by filename only
		$feeds_dir_path = WPPFM_FEEDS_DIR . '/' . $file_name;
		if ( file_exists( $feeds_dir_path ) && is_file( $feeds_dir_path ) ) {
			$real_path = realpath( $feeds_dir_path );
			if ( $real_path ) {
				return $real_path;
			}
		}

		// If directory exists, construct path for new file
		// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_is_writable -- Direct dir checks preferred before WP_Filesystem fallback.
		if ( file_exists( WPPFM_FEEDS_DIR ) && is_dir( WPPFM_FEEDS_DIR ) && is_writable( WPPFM_FEEDS_DIR ) ) {
			$real_feeds_dir = realpath( WPPFM_FEEDS_DIR );
			if ( $real_feeds_dir ) {
				return $real_feeds_dir . '/' . $file_name;
			}
		}
	}

	// If we can't determine the real path, return false to trigger fallback
	return false;
}

/**
 * Checks if the feed update schedule is registered. If it's missing, it will reactivate it again.
 *
 * @since 2.20.0
 */
function wppfm_check_feed_update_schedule() {
	if ( ! wp_get_schedule( 'wppfm_feed_update_schedule' ) ) {
		// add the schedule cron
		wp_schedule_event( time(), 'hourly', 'wppfm_feed_update_schedule' );
	}
}

/**
 * Recursively implodes an array
 *
 * @since 2.8.0
 *
 * @param array|string $array
 * @param string $glue
 * @param bool $include_keys
 * @param bool $trim_all
 *
 * @return string
 */
function wppfm_recursive_implode( $array, $glue = ',', $include_keys = false, $trim_all = true ) {
	$glued_string = '';

	// @since 2.41.0
	if ( ! is_array( $array ) ) {
		$array = json_decode( $array, true );
	}

	// @since 2.41.0
	if ( ! $array ) {
		return '';
	}

	// Recursively iterates array and adds key/value to glued string
	array_walk_recursive(
		$array,
		function ( $value, $key ) use ( $glue, $include_keys, &$glued_string ) {
			$include_keys and $glued_string .= $key . ' => ';
			$glued_string                   .= $value . $glue;
		}
	);

	// Removes last $glue from string
	if ( strlen( $glue ) > 0 && $glued_string ) {
		$glued_string = substr( $glued_string, 0, - strlen( $glue ) );
	}

	// Trim ALL whitespace
	if ( $trim_all && $glued_string ) {
		/** @noinspection PhpRegExpRedundantModifierInspection */
		$glued_string = preg_replace( '/(\s)/ixsm', '', $glued_string );
	}

	return $glued_string;
}

function wppfm_clear_feed_process_data() {
	WPPFM_Feed_Controller::clear_feed_queue();
	WPPFM_Feed_Controller::set_feed_processing_flag();
	WPPFM_Db_Management::clean_options_table();
	WPPFM_Db_Management::reset_status_of_failed_feeds();
	WPPFM_Db_Management::reset_feed_runtime_state();

	do_action( 'wppfm_feed_process_data_cleared' );

	return true;
}

/**
 * Takes a string with spaces and capital letters and converts it to a string with dashes and lower case letters
 *
 * @param $original_string
 *
 * @return string
 */
function wppfm_convert_string_with_spaces_to_lower_case_string_with_dashes( $original_string ) {
	return strtolower( str_replace( ' ', '-', $original_string ) );
}

function wppfm_convert_string_with_dashes_to_upper_case_string_with_spaces( $original_string ) {
	return null !== $original_string ? ucwords( str_replace( '-', ' ', $original_string ) ) : '';
}

/**
 * Converts any number string to a string with a number that has no thousand separators
 * and a period as a decimal separator
 *
 * @param string $number_string
 *
 * @since 2.28.0 Switched to the formal wc functions to get the separator and number of decimal values.
 *
 * @return string
 */
function wppfm_number_format_parse( $number_string ) {
	$decimal_separator  = wc_get_price_decimal_separator();
	$thousand_separator = wc_get_price_thousand_separator();

	// convert a number string that is an actual standard number format whilst the woocommerce options are not standard
	// to the woocommerce standard.
	// This sometimes happens with meta-values
	if ( ! empty( $decimal_separator ) && strpos( $number_string, $decimal_separator ) === false ) {
		$number_string = ! empty( $thousand_separator ) && strpos( $number_string, $thousand_separator ) === false ? $number_string : str_replace( $thousand_separator, $decimal_separator, $number_string );
	}

	$no_thousands_sep = str_replace( $thousand_separator, '', $number_string );

	return '.' !== $decimal_separator ? str_replace( $decimal_separator, '.', $no_thousands_sep ) : $no_thousands_sep;
}

/**
 * returns the path to the feed file including feed name and extension
 *
 * @param string $feed_name
 *
 * @return string
 */
function wppfm_get_file_path( $feed_name ) {
	$forbidden_name_chars = wppfm_forbidden_file_name_characters();
	$feed_name            = str_replace( $forbidden_name_chars, '-', $feed_name );

	// previous to plugin version 1.3.0 feeds where stored in the plugins, but after that version they are stored in the upload folder
	// @since 3.17.0 - Use dirname() to get the plugins directory from the plugin directory.
	$legacy_plugin_path = dirname( WPPFM_PLUGIN_DIR ) . '/wp-product-feed-manager-support/feeds/' . $feed_name;
	if ( file_exists( $legacy_plugin_path ) ) {
		return $legacy_plugin_path;
	} elseif ( file_exists( WPPFM_FEEDS_DIR . '/' . $feed_name ) ) {
		return WPPFM_FEEDS_DIR . '/' . $feed_name;
	} else { // as of version 1.5.0, all spaces in new filenames are replaced by a dash
		return WPPFM_FEEDS_DIR . '/' . $feed_name;
	}
}

/**
 * Returns the url of the feed file including feed name and extension.
 *
 * @param   string  $feed_name  Name of the feed file.
 *
 * @return  string  URL to the feed file.
 */
function wppfm_get_file_url( $feed_name ) {
	$forbidden_name_chars = wppfm_forbidden_file_name_characters();
	$feed_name            = str_replace( $forbidden_name_chars, '-', $feed_name );

	// previous to plugin version 1.3.0 feeds where stored in the plugins, but after that version they are stored in the upload folder
	// @since 3.17.0 - Use dirname() to get the plugins directory from the plugin directory.
	$legacy_plugin_path = dirname( WPPFM_PLUGIN_DIR ) . '/wp-product-feed-manager-support/feeds/' . $feed_name;
	if ( file_exists( $legacy_plugin_path ) ) {
		// Use plugins_url() with the legacy plugin file path for proper URL resolution.
		$legacy_plugin_file = dirname( WPPFM_PLUGIN_FILE ) . '/../wp-product-feed-manager-support/wp-product-feed-manager-support.php';
		$file_url = plugins_url( 'feeds/' . $feed_name, $legacy_plugin_file );
	} else { // as of version 1.5.0, all spaces in new filenames are replaced by a dash
		$file_url = WPPFM_UPLOADS_URL . '/wppfm-feeds/' . $feed_name;
	}

	return apply_filters( 'wppfm_feed_url', $file_url, $feed_name );
}

/**
 * @return array with forbidden characters
 */
function wppfm_forbidden_file_name_characters() {
	return array( ' ', '<', '>', ':', '?', ',', "'", '{', '}', '#' ); // characters that are not allowed in a feed file name
}

/**
 * For backward compatibility, the old feed statuses are converted to all lowercases and without spaces
 *
 * @param array $list
 *
 * @since 2.1.0
 *
 * @noinspection PhpParameterByRefIsNotUsedAsReferenceInspection
 */
function wppfm_correct_old_feeds_list_status( &$list ) {
	for ( $i = 0; $i < count( $list ); $i ++ ) {
		$list[ $i ]->status = strtolower( str_replace( ' ', '_', $list[ $i ]->status ) );
	}
}

/**
 * Checks if the WooCommerce plugin is installed and active
 *
 * @return boolean true if WooCommerce is installed and active, false if not
 * @since 2.3.0
 */
function wppfm_wc_installed_and_active() {
	return is_plugin_active( 'woocommerce/woocommerce.php' ) || is_plugin_active_for_network( 'woocommerce/woocommerce.php' );
}

/**
 * Checks if the WooCommerce plugin has the minimal required version
 *
 * @return boolean true if WooCommerce version is at least 3.0.0
 * @since 2.3.0
 * @since 3.16.0 - Changed the use of WPPFM_PLUGIN_DIR + '..' to find the plugins folder, to the use of WP_PLUGIN_DIR.
 */
function wppfm_wc_min_version_required() {
	// To prevent several PHP Warnings if the WC folder name has been changed whilst the plugin is still registered.
	// @since 2.11.0.
	// Use dirname() to get the plugins directory from the plugin directory.
	$wc_plugin_file = dirname( WPPFM_PLUGIN_DIR ) . '/woocommerce/woocommerce.php';
	if ( ! file_exists( $wc_plugin_file ) ) {
		return false;
	}

	$wc_version = get_plugin_data( $wc_plugin_file )['Version'];

	return version_compare( $wc_version, WPPFM_MIN_REQUIRED_WC_VERSION, '>=' );
}

/**
 * Stores the latest blog data in the WordPress options, maintaining a maximum of four entries.
 *
 * @param array $blog_data An associative array containing the weblog data to store.
 *
 * @return void
 * @since 3.14.0.
 */
function wppfm_store_latest_blog( $blog_data ) {
	$current_weblogs = get_option( 'wppfm_latest_weblogs', array() );

	// Add the new weblog data to the beginning of the array.
	array_unshift( $current_weblogs, $blog_data);

	// Keep only the four latest weblogs.
	$current_weblogs = array_slice( $current_weblogs, 0, 4 );

	// Update the option with the new array.
	update_option( 'wppfm_latest_weblogs', $current_weblogs );
}