<?php
/**
 * Utils class for common functions.
 *
 * @category   WordPress_Plugin
 * @package    Common
 * @subpackage Utils
 * @author     miniOrange <info@miniorange.com>
 * @license    MIT/Expat
 * @link       https://plugins.miniorange.com/woocommerce-api-product-sync-with-woocommerce-rest-apis
 */

namespace MOWCPS\Common\Utils;

use MOWCPS\Common\Constants\Constants;
use MOWCPS\Common\Services\Logging\Supplier_Logger;
use MOWCPS\Common\Models\Sync_Details_DAO;
use MOWCPS\Common\Models\Setting_Model;

/**
 * Utils class for common functions.
 */
class Utils {

	/**
	 * Ensure WP_Filesystem is loaded and initialized.
	 *
	 * @return void
	 */
	private static function ensure_wp_filesystem() {
		if ( ! function_exists( 'WP_Filesystem' ) ) {
			require_once \ABSPATH . 'wp-admin' . DIRECTORY_SEPARATOR . 'includes' . DIRECTORY_SEPARATOR . 'file.php';
		}
		\WP_Filesystem();
	}

	/**
	 * Check if test is successful
	 *
	 * @param array $response Response data.
	 * @return bool True if test is successful, false otherwise.
	 */
	public static function is_status_success( $response ) {
		// Check if response status is successful (2XX).
		$status_code = wp_remote_retrieve_response_code( $response );
		return ( $status_code >= 200 && $status_code < 300 );
	}

	/**
	 * Replace supplier_id placeholder in configuration
	 *
	 * @param array $config Configuration array.
	 * @param int   $supplier_id Numeric supplier ID.
	 * @return array Configuration with supplier_id placeholder replaced.
	 */
	public static function replace_supplier_id_placeholder( $config, $supplier_id ) {
		// Convert config to JSON string for replacement.
		$config_json = wp_json_encode( $config );

		// Replace the supplier_id placeholder with numeric value (remove quotes).
		$config_json  = str_replace( '"<supplier_id>"', $supplier_id, $config_json );
		$final_config = json_decode( $config_json, true );

		return $final_config ? $final_config : $config;
	}

	/**
	 * Runs on plugin activation, will be used to initialize option structures and default values.
	 *
	 * @return void
	 */
	public static function run_on_plugin_activation() {
	}

	/**
	 * Runs on plugin deactivation, will be used to destroy option structures and default values.
	 *
	 * @return void
	 */
	public static function run_on_plugin_deactivation() {
		$option_name  = base64_encode( 'MOWCPS_user_details' ); // phpcs:ignore --WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode Not used for code obfuscation.
		Model_Utils::delete_option( $option_name );
		Model_Utils::delete_option( 'MOWCPS_user_display_details' );
	}

	/**
	 * Get Version number
	 *
	 * @return string
	 */
	public static function get_version_number() {
		$file_data      = get_file_data( MOWCPS_PLUGIN_DIR . Constants::PLUGIN_FILE, array( 'Version' ), 'plugin' );
		$plugin_version = isset( $file_data[0] ) ? $file_data[0] : '1.0.0';
		return $plugin_version;
	}


	/**
	 * Store data in a file and used to create directories recursively if not already created.
	 *
	 * // TODO: Add this function to file_save_handler class.
	 *
	 * @param string $file_name           The name of the file.
	 * @param string $file_directory_path The directory path where the file will be stored.
	 * @param string $file_data           The data to be stored in the file.
	 *
	 * @return bool true|false
	 */
	public static function store_data_in_file( $file_name, $file_directory_path, $file_data = '' ) {
		global $wp_filesystem;
		\WP_Filesystem();

		// Create file directory only if not exists.
		if ( wp_mkdir_p( $file_directory_path ) ) {
			// Generate file path from file_directory_path and file_name.
			$file_path = $file_directory_path . '/' . $file_name;

			// Normalize path for Windows compatibility.
			$file_path = wp_normalize_path( $file_path );

			// Delete existing file if it exists to avoid lock issues on Windows.
			self::delete_file_with_retry( $file_path );

			// Use WP_Filesystem to write the file
			$result = $wp_filesystem->put_contents( $file_path, $file_data, 0644 );

			// WP_Filesystem put_contents returns true on success or false on failure
			return $result ? true : false;
		}
		return false;
	}

	/**
	 * Function to retrieve API response stored in JSON file to minimize API calls.
	 *
	 * @param  string $file_path Path of the file to retrieve data from.
	 * @return string File data.
	 */
	public static function retrieve_data_from_file( $file_path ) {
		global $wp_filesystem;
		self::ensure_wp_filesystem();
		// Check if the file exists.
		if ( $wp_filesystem->exists( $file_path ) ) {
			// Read the file contents.
			$file_data = $wp_filesystem->get_contents( $file_path );
			return $file_data;
		}
	}

	/**
	 * To load all given file paths with curated file path strings.
	 *
	 * @param array $load_files File paths to be loaded.
	 *
	 * @return void
	 */
	public static function load_files( $load_files ) {

		$load_files = self::replace_directory_separator( $load_files );
		foreach ( $load_files as $file ) {
			require_once $file;
		}
	}

	/**
	 * To load all given file paths with curated file path strings.
	 *
	 * @return void
	 */
	public static function load_constants() {

		spl_autoload_register(
			function ( $class_file ) {
				if ( false === strpos( $class_file, 'Constant' ) ) {
					return;
				}
				$path_details = explode( '\\', $class_file );
				unset( $path_details[0] );

				$class_name  = array_pop( $path_details );
				$plan_folder = array_shift( $path_details );

				$class_name = strtolower( $class_name );
				$file_name  = 'class-' . str_replace( '_', '-', $class_name ) . '.php';

				$base_path = trailingslashit( MOWCPS_PLUGIN_DIR ) . 'classes/' . $plan_folder . '/Utils/';

				// Handling exceptional cases.
				if ( empty( $path_details ) ) {
					$base_path = $base_path . 'Constants/';
				}
				foreach ( $path_details as $path ) {
					$base_path = $base_path . $path . '/';
				}

				$file_path = $base_path . $file_name;
				if ( file_exists( $file_path ) ) {
					include_once $file_path;
				}
			}
		);
	}

	/**
	 * Replace file path slashes with directory separator.
	 *
	 * @param array|string $file_list List of file names or a single file name.
	 *
	 * @return array
	 */
	public static function replace_directory_separator( $file_list ) {
		if ( is_array( $file_list ) ) {
			$curated_names = array();
			foreach ( $file_list as $file_name ) {
				// Replace all slashes with the correct directory separator.
				$curated_names[] = str_replace( array( '/', '\\' ), '/', $file_name );
			}
			$file_list = $curated_names;
		} elseif ( is_string( $file_list ) ) {
			// Replace all slashes with the correct directory separator.
			$file_list = str_replace( array( '/', '\\' ), '/', $file_list );
		}

		return (array) $file_list;
	}

	/**
	 * Replace url path slashes with url separator.
	 *
	 * @param array|string $file_list List of file names or a single file name.
	 *
	 * @return array|string
	 */
	public static function replace_url_separator( $file_list ) {
		if ( is_array( $file_list ) ) {
			$curated_names = array();
			foreach ( $file_list as $file_name ) {
				// Replace all slashes with the correct directory separator.
				$curated_names[] = str_replace( '\\', URL_SEPARATOR, $file_name );
			}
			$file_list = $curated_names;
		} elseif ( is_string( $file_list ) ) {
			// Replace all slashes with the correct directory separator.
			$file_list = str_replace( '\\', URL_SEPARATOR, $file_list );
		}

		return $file_list;
	}

	/**
	 * Deletes the test result file for the specified API as a callback for a daily cron job.
	 *
	 * @param string $connection_name Name of the external API connection.
	 *
	 * @return void
	 */
	public static function delete_test_result_file( $connection_name ) {
		$file_path = rtrim( MOWCPS_PLUGIN_DIR, '/' ) . DIRECTORY_SEPARATOR . 'data' . DIRECTORY_SEPARATOR . 'test-results' . DIRECTORY_SEPARATOR . 'test_result_' . $connection_name . '.json';

		global $wp_filesystem;
		self::ensure_wp_filesystem();

		if ( $wp_filesystem->exists( $file_path ) ) {
			$wp_filesystem->delete( $file_path );
		}
	}

	/**
	 * Checks if class exist and if not gives a Common's class name instead.
	 *
	 * @param string $class_namespace Namespace of class.
	 * @param string $class_name      Only class name.
	 *
	 * @return string
	 * */
	public static function validate_class_name( $class_namespace, $class_name ) {

		$complete_class_name = $class_namespace . $class_name;
		$return_class_name   = $complete_class_name;

		if ( ! class_exists( $complete_class_name ) ) {
			$pattern                = '/(?<=MOWCPS\\\)[^\\\]+(?=\\\)/';
			$replacement            = 'Common';
			$common_class_namespace = preg_replace( $pattern, $replacement, $class_namespace );

			$complete_class_name_common = $common_class_namespace . $class_name;

			if ( class_exists( $complete_class_name_common ) ) {
				$return_class_name = $complete_class_name_common;
			}
		}

		return $return_class_name;
	}

	/**
	 * Get the current URL and check if the current page is valid for saving configuration or not.
	 *
	 * @return bool
	 */
	public static function is_save_configuration_page() {
		$path = ! empty( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '';
		if ( ( strpos( $path, 'page=sync-to-woocommerce' ) !== false ) && ( ! empty( $_SERVER['REQUEST_METHOD'] ) && 'POST' === sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) ) ) {
			return true;
		}
		return false;
	}

	/**
	 * Get the value of a parameter from the current URL.
	 *
	 * @param string $parameter_name Name of the parameter to get the value of.
	 * @param string $default_value Default value to return if the parameter is not found.
	 * @return string
	 */
	public static function get_current_page_parameter_value( $parameter_name, $default_value = '' ) {
		$path = home_url( add_query_arg( null, null ) );

		$parameter_value = '';

		// Parse the URL to get the query string.
		$query_str = wp_parse_url( $path, PHP_URL_QUERY );

		// Parse the query string into an array.
		if ( $query_str ) {
			parse_str( $query_str, $query_params );

			// Get the parameter value if it exists.
			if ( ! empty( $query_params[ $parameter_name ] ) ) {
				$parameter_value = $query_params[ $parameter_name ];
			}
			unset( $query_params );
		}

		return ! empty( $parameter_value ) ? $parameter_value : $default_value;
	}

	/**
	 * Wrapper func. to safely JSON-encodes any data.
	 *
	 * @param  mixed $data The data to encode.
	 * @return string JSON string on success, or error message on failure.
	 */
	public static function safe_json_encode( $data ) {
		try {
			return wp_json_encode( $data, JSON_THROW_ON_ERROR );
		} catch ( \JsonException $e ) {
			$error_msg   = 'MOWCPS_ERROR_EXCEPTION - JSON Encode Error: ' . $e->getMessage();
			$stack_trace = $e->getTraceAsString();
			Supplier_Logger::debug( Sync_Details_DAO::get_instance()->get_supplier_id(), $error_msg . "\nStack trace:\n" . $stack_trace );
			// return $error_msg;
			return '';
		}
	}

	/**
	 * Wrapper func. to safely JSON-decodes a string into an associative array.
	 *
	 * @param  string $json The JSON string to decode.
	 * @return array Associative array on success, or error message on failure.
	 */
	public static function safe_json_decode( string $json ) {
		try {
			@ini_set( 'memory_limit', '2G' );

			$file_sync_details = Setting_Model::get_by_connection_and_operation( Sync_Details_DAO::get_instance()->get_connection_id(), 'file-sync', array( 'configuration' => true ) );

			if ( isset( $file_sync_details['is_active'] ) && '1' === $file_sync_details['is_active'] ) {
				return $json;
			}
			return json_decode( $json, true, 512, JSON_THROW_ON_ERROR );        } catch ( \JsonException $e ) {
			$error_msg   = 'MOWCPS_ERROR_EXCEPTION - JSON Decode Error: ' . $e->getMessage();
			$stack_trace = $e->getTraceAsString();
			// Debug_Logger::edit_log( $error_msg . "\nStack trace:\n" . $stack_trace );
			// return array( $error_msg );
			return array();
			}
	}

	/**
	 * Split the input string into array using '+' as the separator. '+' in ## ## is exception and is accepted as static.
	 *
	 * @param string $input_string Input string to match regex.
	 *
	 * @return array|boolean
	 */
	public static function split_to_array( $input_string ) {
		$regex = '/\+(?=(?:[^#]*##[^#]*##)*[^#]*$)/';
		return preg_split( $regex, $input_string, -1, PREG_SPLIT_NO_EMPTY );
	}

	/**
	 * Generate request parameters
	 *
	 * @param array $additional_headers Additional headers.
	 * @param array $override_headers Override headers.
	 * @param array $additional_args Additional arguments.
	 * @param array $override_args Override arguments.
	 * @return array
	 */
	public static function generate_request_parameters( $additional_headers = array(), $override_headers = array(), $additional_args = array(), $override_args = array() ) {
		$headers = array(
			'Content-Type'  => 'application/json',
			'charset'       => 'UTF - 8',
			'Authorization' => 'Basic',
		);

		$headers = ( $override_headers && $additional_headers ) ? $additional_headers : array_unique( array_merge( $headers, $additional_headers ) );

		$args = array(
			'timeout'     => '20',
			'redirection' => '5',
			'httpversion' => '1.0',
			'blocking'    => true,
			'sslverify'   => true,
		);

		$args = ( $override_args && $additional_args ) ? $additional_args : array_unique( array_merge( $args, $additional_args ) );

		return array(
			'headers' => $headers,
			'args'    => $args,
		);
	}

	/**
	 * Sanitize JSON field data while preserving nested structures
	 *
	 * @param mixed $data Data to sanitize.
	 * @param array $context Context information for sanitization.
	 * @return mixed Sanitized data with preserved structure.
	 */
	public static function sanitize_json_field( $data, $context = array() ) {
		if ( is_array( $data ) ) {
			$sanitized = array();
			foreach ( $data as $key => $value ) {
				$sanitized_key = sanitize_text_field( $key );

				// Pass context down to handle body and position data appropriately.
				$new_context                  = $context;
				$new_context['in_body']       = ( 'body' === $key && is_array( $value ) ) ? true : ( $new_context['in_body'] ?? false );
				$new_context['body_type']     = ( 'body' === $key && is_array( $value ) ) ? ( $value['type'] ?? 'json' ) : ( $new_context['body_type'] ?? 'json' );
				$new_context['in_position']   = ( 'position' === $key && is_array( $value ) ) ? true : ( $new_context['in_position'] ?? false );
				$new_context['position_type'] = ( 'position' === $key && is_array( $value ) ) ? ( $value['type'] ?? 'json' ) : ( $new_context['position_type'] ?? 'json' );

				if ( isset( $context['in_body'] ) && 'data' === $key ) {
					$new_context['is_body_data'] = true;
				}

				if ( isset( $context['in_position'] ) && 'data' === $key ) {
					$new_context['is_position_data'] = true;
				}

				if ( isset( $context['is_position_data'] ) && 'textarea' === $key ) {
					$new_context['is_position_textarea'] = true;
				}

				if ( 'url' === $key || 'endpoint' === $key ) {
					$sanitized[ $sanitized_key ] = $value;
				} else {
					$sanitized[ $sanitized_key ] = self::sanitize_json_field( $value, $new_context );
				}
			}
			return $sanitized;
		} elseif ( is_string( $data ) ) {
			// Check if this is XML body data that should not be sanitized.
			if ( isset( $context['is_body_data'] ) && isset( $context['body_type'] ) && in_array( $context['body_type'], array( 'xml', 'graphql', 'raw' ), true ) ) {
				// For XML body types, trim whitespace but preserve the data structure.
				if ( 'xml' === $context['body_type'] ) {
					return trim( $data );
				}
				// For GraphQL and raw body types, preserve the data as-is.
				return $data;
			}

			// Check if this is XML position textarea data that should not be sanitized.
			if ( isset( $context['is_position_textarea'] ) && isset( $context['position_type'] ) && in_array( $context['position_type'], array( 'xml', 'graphql', 'raw' ), true ) ) {
				// For XML position types, trim whitespace but preserve the data structure.
				if ( 'xml' === $context['position_type'] ) {
					return trim( $data );
				}
				// For GraphQL and raw position types, preserve the data as-is.
				return $data;
			}

			return sanitize_text_field( $data );
		} elseif ( is_numeric( $data ) ) {
			return $data;
		} elseif ( is_bool( $data ) ) {
			return $data;
		}

		return $data;
	}

	/**
	 * Check if a array is nested or not.
	 *
	 * @param array $array_to_check Input array.
	 */
	public static function is_flat_array( $array_to_check ) {
		if ( ! is_array( $array_to_check ) ) {
			return false;
		}

		foreach ( $array_to_check as $value ) {
			if ( is_array( $value ) || is_object( $value ) ) {
				return false;
			}
		}

		return true;
	}

	/**
	 * Flattens a multidimensional array into a single-level array.
	 *
	 * @param array  $array  The multidimensional array to be flattened.
	 * @param string $prefix The prefix to prepend to the keys.
	 * @return array The flattened array.
	 */
	public static function flatten_array( $array, $prefix = '' ) {
		$result = array();

		foreach ( $array as $key => $value ) {
			$new_key = $prefix ? $prefix . '->' . $key : $key;

			if ( is_array( $value ) ) {
				$result = array_merge( $result, self::flatten_array( $value, $new_key ) );
			} else {
				$result[ $new_key ] = $value;
			}
		}

		return $result;
	}

	/**
	 * Locate and store paused sync cron event by supplier & schedule.
	 * This allows restoring later via resume.
	 */
	public static function store_paused_sync_cron_event( $supplier_id, $schedule_id ) {
		$cron_jobs = get_option( 'cron' );

		if ( empty( $cron_jobs ) ) {
			return;
		}

		foreach ( $cron_jobs as $timestamp => $cron ) {
			if ( isset( $cron['mowcps_product_sync_backup_cron'] ) ) {
				foreach ( $cron['mowcps_product_sync_backup_cron'] as $key => $event ) {
					$args = $event['args'][0];

					if ( $args['supplier_id'] == $supplier_id && $args['schedule_id'] == $schedule_id ) {
						// Store event args in option so resume can pick it later
						Model_Utils::update_option(
							'mowcps_paused_cron_' . $supplier_id . '_' . $schedule_id,
							$args
						);
					}
				}
			}
		}
	}

	/**
	 * Determine next cron frequency (in seconds).
	 *
	 * @return int
	 */
	public static function get_next_cron_frequency() {
		$max_exec_time = empty( ini_get( 'max_execution_time' ) ) ? 180 : ini_get( 'max_execution_time' );
		$next_cron     = ( $max_exec_time < 180 || $max_exec_time > 600 ) ? ( 180 / 2 ) : ( $max_exec_time / 2 );

		$frequencies                                    = Model_Utils::get_option( 'mowcps_frequencies', array() );
		$frequencies[ 'mowcps_' . $next_cron . '_sec' ] = $next_cron;
		Model_Utils::update_option( 'mowcps_frequencies', $frequencies );
		return $next_cron;
	}

	/**
	 * Detect delimiter for CSV/TSV/TXT files based on file type.
	 *
	 * @param string $file_type File type (csv, tsv, txt).
	 * @return string Delimiter character.
	 */
	public static function detect_file_delimiter( $file_type ) {
		if ( Constants::TSV_FILE === $file_type ) {
			return "\t";
		} elseif ( Constants::CSV_FILE === $file_type ) {
			return ',';
		} else {
			return '|';
		}
	}

	/**
	 * Delete a file with retry mechanism to handle Windows file locking issues.
	 *
	 * Attempts to delete a file multiple times with delays between attempts.
	 * This is necessary on Windows where file locking can be persistent.
	 * Note: This function does not return a value as the result is not used.
	 * The deletion is attempted to avoid lock issues, but put_contents() will
	 * overwrite the file even if deletion fails.
	 *
	 * @param string $file_path Full path to the file to delete.
	 * @param int    $max_attempts Maximum number of deletion attempts. Default is 5.
	 * @param int    $delay_usec Delay between attempts in microseconds. Default is 100000 (100ms).
	 * @return void
	 */
	public static function delete_file_with_retry( $file_path, $max_attempts = 5, $delay_usec = 100000 ) {
		// Validate input parameters.
		if ( empty( $file_path ) || ! is_string( $file_path ) ) {
			return;
		}

		// Validate max_attempts and delay_usec are positive.
		$max_attempts = max( 1, (int) $max_attempts );
		$delay_usec   = max( 0, (int) $delay_usec );

		global $wp_filesystem;
		self::ensure_wp_filesystem();

		// Normalize path for Windows compatibility.
		$file_path = wp_normalize_path( $file_path );

		// If file doesn't exist, nothing to delete.
		if ( ! $wp_filesystem->exists( $file_path ) ) {
			return;
		}

		$delete_attempts = 0;

		// Try multiple times to ensure file is deleted (Windows file locking can be persistent).
		while ( $delete_attempts < $max_attempts ) {
			// Attempt to delete the file.
			$wp_filesystem->delete( $file_path );

			// Clear stat cache to ensure we get fresh file status.
			clearstatcache( true, $file_path );

			// Check if file was successfully deleted.
			if ( ! $wp_filesystem->exists( $file_path ) ) {
				return;
			}

			// Increment attempt counter.
			++$delete_attempts;

			// Delay before next attempt (skip delay on last attempt).
			if ( $delete_attempts < $max_attempts ) {
				usleep( $delay_usec );
			}
		}
	}

	/**
	 * Read data from XLSX file.
	 *
	 * TODO: RESTAPI-371 - Fix XLSX reading flow. This function needs to be reviewed and improved.
	 * data type normalization, empty row handling, and sheet selection limitations.
	 *
	 * @param string    $file_path         Full path to the XLSX file.
	 * @param bool      $should_limit_lines Whether to limit lines.
	 * @param int|false $start_line        Line number to start reading from (1-based index).
	 * @param int|false $end_line          Line number to stop reading at (inclusive).
	 * @return array Array of rows from the XLSX file.
	 */
	public static function read_xlsx_file( $file_path, $should_limit_lines, $start_line, $end_line ) {
		$spreadsheet = \PhpOffice\PhpSpreadsheet\IOFactory::load( $file_path );
		$worksheet   = $spreadsheet->getActiveSheet();

		$data = array();

		// If no line filtering, return all.
		if ( ! $should_limit_lines ) {
			$data = $worksheet->toArray();
		} else {
			$row_iterator = $worksheet->getRowIterator( $start_line, $end_line );
			foreach ( $row_iterator as $row ) {
				$cell_iterator = $row->getCellIterator();
				$cell_iterator->setIterateOnlyExistingCells( false );
				$row_data = array();
				foreach ( $cell_iterator as $cell ) {
					$row_data[] = $cell->getValue();
				}
				$data[] = $row_data;
			}
		}

		return $data;
	}
}
