<?php
/**
 * VertivProducts class definition
 *
 * This class handles the creation of the CommonFrameworkPlugin post type and manages the lifecycle of products.
 *
 * @package CommonFrameworkPlugin
 */

// phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase

namespace CommonFrameworkPlugin;

use CommonFrameworkPlugin\VertivXMLImporter;
use FacetWP;

/**
 * Handles all operations related to Vertiv products within the CommonFrameworkPlugin.
 */
class VertivProducts extends \CommonFrameworkPlugin\Module {

	/**
	 * Stores products from the database
	 *
	 * @var array Stores products from the database
	 * */
	private $db_products;

	/**
	 * Whether or function is running from CLI
	 *
	 * @var bool CLI detection
	 * */
	private $cli;

	/**
	 * Checks if registration conditions are met.
	 *
	 * @return bool True if registration can proceed, false otherwise.
	 */
	public function can_register() {
		return true;
	}

	/**
	 * Sets up the necessary hooks for this module.
	 *
	 * @return void
	 */
	public function register() {
		$this->db_products = null;
		$this->cli = defined( 'WP_CLI' ) && WP_CLI;

		add_action( 'admin_menu', [ $this, 'add_cf_tools_page' ] );

		if ( 'https://commonframework.us' === home_url() ) {
			add_action( 'admin_post_import_vertiv_products', [ $this, 'import_vertiv_products' ] );
			add_action( 'admin_post_cron_job_report', [ $this, 'cron_job_report' ] );
			add_action( 'admin_post_clean_duplicates', [ $this, 'clean_duplicate_products' ] );
			add_action( 'save_post', [ $this, 'import_vertiv_data' ], 10, 3 );
			add_action( 'acf/save_post', [ $this, 'discontinue_product' ], 20 );
			add_filter( 'cron_schedules', [ $this, 'add_scheduled_interval' ] );
			if ( ! wp_next_scheduled( 'vf_schedule' ) ) {
				wp_schedule_event( time(), 'weekly', 'vf_schedule' );
			}

			add_action( 'vf_schedule', [ $this, 'cron_job_report' ] );
		}

		add_filter( 'facetwp_templates', [ $this, 'register_facet_templates' ], 10, 1 );
		add_filter( 'facetwp_facets', [ $this, 'broadcast_facets' ], 10, 1 );
		add_filter( 'facetwp_index_row', [ $this, 'filter_facet_indexer' ], 20, 2 );
	}

	/**
	 * Adds a menu page for Common Framework tools.
	 */
	public function add_cf_tools_page() {
		if ( empty( $GLOBALS['admin_page_hooks']['cf_tools'] ) ) {
			add_menu_page(
				'CF Tools',
				'CF Tools',
				'manage_options',
				'cf_tools',
				[ $this, 'cf_tools_page' ],
				'dashicons-admin-network',
				2
			);
		}
	}

	/**
	 * Displays the CF Tools page content.
	 */
	public function cf_tools_page() {
		include COMMON_FRAMEWORK_PLUGIN_INC . 'pages/tools.php';
	}

	/**
	 * Adds a weekly interval to WordPress cron schedules.
	 *
	 * @param array $schedules Existing cron schedules.
	 * @return array Modified schedules with a weekly interval added.
	 */
	public function add_scheduled_interval( $schedules ) {
		$schedules['weekly'] = [
			'interval' => 604800,
			'display'  => 'Once every week',
		];
		return $schedules;
	}

	/**
	 * Checks if a product is sold in the specified region and countries.
	 *
	 * @param object $product Product to check.
	 * @param string $correct_region Target region.
	 * @param string $correct_countries Target countries.
	 * @return bool True if sold in the specified location, false otherwise.
	 */
	public function is_product_sold_in_region( $product, $correct_region, $correct_countries ) {
		$regions_sold_in = (string) $product->RegionsSoldIn;
		$countries_sold_in = (string) $product->CountriesSoldIn;

		if ( '-- Not Applicable --' !== $regions_sold_in && false !== strpos( $regions_sold_in, $correct_region ) ) {
			return true;
		}

		if ( '-- Not Applicable --' !== $countries_sold_in ) {
			$countries_array = explode( ',', $countries_sold_in );
			if ( in_array( $correct_countries, $countries_array ) ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Ensures a product is not part of a blacklisted category.
	 *
	 * @param object $product Product to check.
	 * @return bool True if product is not blacklisted, false otherwise.
	 */
	public function is_product_in_correct_category( $product ) {
		$product_group = (string) $product->product_group;
		return 'Services' !== $product_group;
	}

	/**
	 * Retrieves a post by meta data.
	 *
	 * @param array $args Query arguments for retrieving the post.
	 * @return object|bool WP post object if found, false otherwise.
	 */
	public function get_post_by_meta( $args = [] ) {
		$args = [
			'meta_query' => [
				[
					'key'   => $args['meta_key'],
					'value' => $args['meta_value'],
				],
			],
			'post_type'      => 'products',
			'posts_per_page' => '1',
			'post_status'    => 'any',
		];

		$posts = get_posts( $args );

		if ( ! $posts || is_wp_error( $posts ) ) {
			return false;
		}

		return $posts[0];
	}

	/**
	 * Retrieves all stored products from the database.
	 *
	 * @return array An array of Product objects from the database.
	 */
	private function get_products_from_database() {
		$args = [
			'post_type'      => [ 'products' ],
			'post_status'    => [ 'publish', 'draft' ],
			'posts_per_page' => '-1',
			'meta_query'     => [
				'key' => 'product_id',
			],
		];

		if ( $this->cli ) {
			WP_CLI::line( 'Getting all products...' );
		}

		$products = get_posts( $args );

		if ( $this->cli ) {
			WP_CLI::line( 'Products retrieved...' );
		}

		if ( ! $products || is_wp_error( $products ) ) {
			return [];
		}

		foreach ( $products as $index => $product ) {
			$pid = get_field( 'product_id', $product->ID );
			$products[ $index ]->product_id = $pid;
		}

		return $products;
	}

	/**
	 * Removes duplicate products from the database.
	 */
	public function clean_duplicate_products() {
		$exception_ids = [ 999999, 99999 ];

		$args = [
			'post_type'      => [ 'products' ],
			'post_status'    => [ 'any' ],
			'posts_per_page' => '-1',
			'meta_query'     => [ 'key' => 'product_id' ],
			'fields'         => 'ids',
		];

		$products = get_posts( $args );

		if ( ! $products || is_wp_error( $products ) ) {
			$redirect_url = get_site_url() . '/wp-admin/admin.php?page=cf_tools';
			wp_safe_redirect( $redirect_url );
			exit;
		}

		$duplicate_product_ids = [];
		foreach ( $products as $product ) {
			$prod_id = get_field( 'product_id', $product->ID );
			if ( ! in_array( $prod_id, $exception_ids ) ) {
				$duplicate_product_ids[ $prod_id ][] = $product->ID;
			}
		}

		foreach ( $duplicate_product_ids as $product_id => $post_ids ) {
			$post_id_count = count( $post_ids );
			if ( $post_id_count > 1 ) {
				for ( $i = 1; $i < $post_id_count; $i++ ) {
					wp_trash_post( $post_ids[ $i ] );
				}
			}
		}

		$redirect_url = get_site_url() . '/wp-admin/admin.php?page=cf_tools';
		wp_safe_redirect( $redirect_url );
		exit;
	}

	/**
	 * Removes duplicates from an array based on a specific key.
	 *
	 * @param array  $array The array to process.
	 * @param string $key   The key to check for duplicates.
	 * @return array The filtered array.
	 */
	public function remove_duplicates_from_array( $array, $key ) {
		$temp_array = [];
		$i = 0;
		$key_array = [];

		foreach ( $array as $val ) {
			if ( ! in_array( $val[ $key ], $key_array ) ) {
				$key_array[ $i ] = $val[ $key ];
				$temp_array[ $i ] = $val;
			}
			$i++;
		}
		return $temp_array;
	}

	/**
	 * Imports Vertiv products.
	 */
	public function import_vertiv_products() {
		$xml_importer = new VertivXMLImporter();
		$import = $xml_importer->import();
		if ( is_wp_error( $import ) ) {
			write_log( $import->get_error_message() );
		}

		if ( ! empty( $_POST['action'] ) && 'import_vertiv_products' === $_POST['action'] ) {
			$redirect_url = get_site_url() . '/wp-admin/admin.php?page=cf_tools';
			wp_safe_redirect( $redirect_url );
			exit;
		}
	}

	/**
	 * Retrieves all XML files for Vertiv products.
	 *
	 * @return array Array of XML files.
	 */
	public function get_xml_files() {
		$xml_importer = new VertivXMLImporter();
		return $xml_importer->get_xml_files();
	}

	/**
	 * Imports Vertiv data for a specific post.
	 *
	 * @param int $post_id The ID of the post to import data for.
	 */
	public function import_vertiv_data( $post_id ) {
		$this->assign_parent_terms( $post_id );

		if ( get_field( 'import_feed_data', $post_id ) && get_field( 'product_id', $post_id ) && ! get_field( 'hosted_by_vf', $post_id ) ) {
			$id = get_field( 'product_id', $post_id );
			$child_ids = get_field( 'sub_products', $post_id );

			$child_products_to_update = [];
			$child_products_to_create = [];

			if ( ! empty( $child_ids ) ) {
				$child_products = $this->get_child_product_arrays( $child_ids );
				$child_products_to_update = $child_products[0];
				$child_products_to_create = $child_products[1];
			}

			$products = $this->get_all_xml_products();

			wp_defer_comment_counting( true );
			wp_defer_term_counting( true );

			foreach ( $products as $value ) {
				$product_id = (string) $value->Product->ProductId;

				if ( $product_id == $id ) {
					$this->update_product_posts( $value, $post_id );
					update_field( 'import_feed_data', false, $post_id );
				}

				if ( ! empty( $child_products_to_update ) && count( $child_products_to_update ) > 0 ) {
					if ( array_search( $product_id, array_column( $child_products_to_update, 'product_id' ) ) ) {
						$key = array_search( $product_id, array_column( $child_products_to_update, 'product_id' ) );
						$this->update_product_posts( $value, $child_products_to_update[ $key ]['post_id'], $terms );
					}
				}

				if ( ! empty( $child_products_to_create ) && count( $child_products_to_create ) > 1 ) {
					if ( in_array( $product_id, $child_products_to_create ) ) {
						$this->create_product_posts( $value, $terms );
					}
				}
			}

			wp_defer_comment_counting( false );
			wp_defer_term_counting( false );

			$fwp = \FacetWP::instance();
			$fwp->indexer->index();
		}
	}

	/**
	 * Discontinues a product if the appropriate checkbox is checked.
	 *
	 * @param int $post_id The ID of the post to discontinue.
	 */
	public function discontinue_product( $post_id ) {
		if ( get_field( 'discontinue_product', $post_id ) ) {
			$this->download_documents( $post_id );
			$discontinued_term = get_term_by( 'slug', 'discontinued', 'product-category' );

			wp_set_post_terms( $post_id, array( $discontinued_term->term_id ), 'product-category', true );
			update_field( 'discontinue_product', false, $post_id );
		}
	}

	/**
	 * Downloads documents for a post.
	 *
	 * @param int    $post_id The ID of the post.
	 * @param string $url The URL to download documents from.
	 */
	public function download_documents( $post_id, $url = '' ) {
		if ( empty( $url ) ) {
			$url = home_url();
		}

		$upload_dir = wp_upload_dir();
		$document_directory = $upload_dir['basedir'] . '/products/documents';
		$document_directory_uri = $document_directory . '/';
		$web_root = str_replace( '/wp/', '', ABSPATH );
		$relative_path = str_replace( $web_root, '', $document_directory_uri );

		if ( ! file_exists( $document_directory ) ) {
			wp_mkdir_p( $document_directory );
		}

		if ( have_rows( 'product_documents', $post_id ) ) {
			while ( have_rows( 'product_documents', $post_id ) ) {
				the_row();
				$document_details = get_sub_field( 'document' );
				$file_name = basename( $document_details );

				if ( strpos( $document_details, $relative_path ) === false ) {
					if ( ! file_exists( $document_directory_uri . $file_name ) ) {
						$fp = fopen( $document_details, 'r' );
						file_put_contents( $document_directory_uri . $file_name, $fp );
						fclose( $fp );
					}

					$updated_value = $relative_path . $file_name;
					update_sub_field( 'document', $updated_value, $post_id );
				}
			}
		}
	}

	/**
	 * Assigns parent terms to all high-level terms selected.
	 *
	 * @param int $post_id The post ID of the post that is saved.
	 * @return void
	 */
	public function assign_parent_terms( $post_id ) {
		$post_obj = get_post( $post_id );

		if ( 'products' != $post_obj->post_type ) {
			return;
		}

		$terms = wp_get_post_terms( $post_id, 'product-category' );

		foreach ( $terms as $term ) {
			while ( 0 != $term->parent && ! has_term( $term->parent, 'product-category', $post_obj ) ) {
				wp_set_post_terms( $post_id, array( $term->parent ), 'product-category', true );
				$term = get_term( $term->parent, 'product-category' );
			}
		}
	}

	/**
	 * Checks which child products need to be updated or created.
	 *
	 * @param string $child_ids The child product IDs.
	 * @return array An array containing products to update and products to create.
	 */
	public function get_child_product_arrays( $child_ids ) {
		$child_products_to_update = array();
		$child_products_to_create = array();

		$child_ids_array = explode( ',', str_replace( ' ', '', $child_ids ) );

		$i = 0;
		$k = 0;

		if ( is_null( $this->db_products ) ) {
			$this->db_products = $this->get_products_from_database();
		}

		foreach ( $child_ids_array as $child_id ) {
			$child_product = $this->check_if_product_exists( $child_id, $this->db_products );

			if ( is_object( $child_product ) ) {
				$child_products_to_update[ $i ]['post_id'] = $child_product->ID;
				$child_products_to_update[ $i ]['product_id'] = $child_id;
				$i++;
			} else {
				$child_products_to_create[ $k ] = $child_id;
				$k++;
			}
		}

		return array( $child_products_to_update, $child_products_to_create );
	}

	/**
	 * Checks if a product exists based on its product ID.
	 *
	 * @param int   $product_id The product ID being searched for.
	 * @param array $products The array of products.
	 * @return object|bool The product if found, or false if not found.
	 */
	public function check_if_product_exists( $product_id, $products ) {
		foreach ( $products as $product ) {
			if ( isset( $product->product_id ) && $product->product_id == $product_id ) {
				return $product;
			}
		}
		return false;
	}

	/**
	 * Updates or creates posts for existing products.
	 *
	 * @param SimpleXMLElement $value XML data for the product.
	 * @param int              $post_id ID of the post to update.
	 * @param array            $terms Optional. Array of term data to associate with the post.
	 */
	public function update_product_posts( $value, $post_id, $terms = array() ) {
		wp_suspend_cache_addition( true );
		if ( ! isset( $value ) || ! isset( $post_id ) ) {
			if ( $this->cli ) {
				WP_CLI::line( '[ERROR] Missing data required for product post update. Aborting update process...' );
			}
			return;
		}

		$name = 'Blank Product Title';

		if ( isset( $value->Product->ProductName ) ) {
			$name = (string) $value->Product->ProductName;
		}
		if ( isset( $value->Product->ProductDescription ) ) {
			$description = (string) $value->Product->ProductDescription;
		}

		remove_action( 'save_post', array( $this, 'import_vertiv_data' ) );
		$post = array(
			'ID'           => $post_id,
			'post_title'   => $name,
			'post_content' => $description,
		);

		if ( $this->cli ) {
			WP_CLI::line( 'Updating ' . $post['post_title'] );
		}

		wp_update_post( $post );
		if ( ! empty( $terms ) ) {
			if ( $this->cli ) {
				WP_CLI::line( 'Terms is not empty...' );
			}
			$term_ids = array();
			foreach ( $terms as $term ) {
				array_push( $term_ids, $term->term_id );
			}
			wp_set_post_terms( $post_id, $term_ids, 'product-category', false );
		}

		// $this->stop_the_insanity();
		if ( $this->cli ) {
			WP_CLI::line( 'Updating ACF and Facets for ' . trim( $post['post_title'] ) . '...' );
		}
		$this->update_acf_and_facets_from_product( $value->Product, $post_id );
		$this->update_sub_fields( $value, $post_id );

		wp_suspend_cache_addition( false );
	}


	/**
	 * Creates new product posts based on provided XML data.
	 *
	 * @param SimpleXMLElement $value XML data for creating the post.
	 * @param array            $terms Optional. Terms to associate with the new post.
	 */
	public function create_product_posts( $value, $terms = array() ) {
		wp_suspend_cache_addition( true );
		if ( ! isset( $value ) ) {
			if ( $this->cli ) {
				WP_CLI::line( '[ERROR] Missing product data for post creation. Aborting creation process...' );
			}
			return;
		}

		$id = (string) $value->Product->ProductId;
		$name = 'Blank Product Title';
		$description = '';

		if ( is_null( $this->db_products ) ) {
			$this->db_products = $this->get_products_from_database();
		}

		if ( $this->check_if_product_exists( $id, $this->db_products ) == false ) {
			if ( isset( $value->Product->ProductName ) ) {
				$name = (string) $value->Product->ProductName;
			}
			if ( isset( $value->Product->ProductDescription ) ) {
				$description = (string) $value->Product->ProductDescription;
			}
			remove_action( 'save_post', array( $this, 'import_vertiv_data' ) );
			$post = array(
				'post_type'    => 'products',
				'post_title'   => $name,
				'post_content' => $description,
			);

			if ( $this->cli ) {
				WP_CLI::line( 'Creating ' . $post['post_title'] );
			}

			$post_id = wp_insert_post( $post );
			if ( is_wp_error( $post_id ) || 0 === $post_id ) {
				if ( $this->cli ) {
					WP_CLI::line( '[ERROR] Error during post creation. Post ID could not be properly retrieved...' );
				}
				return;
			}

			if ( ! empty( $terms ) ) {
				wp_set_post_terms( $post_id, $terms, 'product-category', true );
			}

			$this->stop_the_insanity();
			if ( $this->cli ) {
				WP_CLI::line( 'Updating ACF and Facets for ' . trim( $post['post_title'] ) . '...' );
			}

			$this->update_acf_and_facets_from_product( $value->Product, $post_id );
			$this->update_sub_fields( $value, $post_id );
		}
		wp_suspend_cache_addition( false );
	}

	/**
	 * Ensures global database and object caches are cleaned up to prevent memory issues during large operations.
	 */
	public function stop_the_insanity() {
		global $wpdb, $wp_object_cache;
		$wpdb->queries = array();
		if ( is_object( $wp_object_cache ) ) {
			$wp_object_cache->group_ops = array();
			$wp_object_cache->stats = array();
			$wp_object_cache->memcache_debug = array();
			$wp_object_cache->cache = array();
			if ( method_exists( $wp_object_cache, '__remoteset' ) ) {
				$wp_object_cache->__remoteset();
			}
		}
	}

	/**
	 * Updates or creates ACF and FacetWP fields based on product data.
	 *
	 * @param object $product Product data.
	 * @param int    $post_id ID of the post associated with the product.
	 */
	public function update_acf_and_facets_from_product( $product, $post_id ) {

		$group = $this->get_field_group_from_acf( 'Products' );
		if ( null === $group ) {
			if ( $this->cli ) {
				WP_CLI::line( '[ERROR] Unable to retrieve ACF field group...' );
			}
			return;
		}
		acf_flush_field_group_cache( $group );
		$facet_field = $this->get_field_from_acf_group( 'facets', $group );
		if ( null === $facet_field ) {
			if ( $this->cli ) {
				WP_CLI::line( '[ERROR] Unable to retrieve facet field from ACF...' );
			}
			return;
		}

		$product_facets = $this->get_facets_from_product( $product, $post_id );
		$this->log_strategy( 'Facets', $product_facets );
		if ( ! is_array( $product_facets ) || 1 > count( $product_facets ) ) {
			if ( $this->cli ) {
				WP_CLI::line( '[ERROR] Malformed or empty product facets detected, aborting...' );
			}
			return;
		}

		foreach ( $product_facets as $facet ) {
			// $check = $this->find_facet_field_in_acf_subfields($facet_field[ 'sub_fields' ], $facet[ 'label' ]);
			$formatted_label = $this->format_acf_label_name( $facet['label'] );
			if ( empty( $formatted_label ) ) {
				if ( $this->cli ) {
					WP_CLI::line( '[ERROR] Empty facet label detected; skipping...' );
				}
				continue;
			}

			$check = $this->check_for_existing_subfield_name_in_acf( $formatted_label, $group, $facet_field );

			// If the facet field does not exist, create it
			if ( $check < 0 ) {
				if ( $this->cli ) {
					WP_CLI::line( 'Facet Not Found... Creating ACF Field for ' . trim( $facet['label'] ) . '...' );
				}
				$new_subfield = $this->format_new_acf_facet_field( $facet['label'], $facet['tags'], $facet_field['ID'] );
				$this->update_acf_parent_and_child_fields( $group, $facet_field, $new_subfield, $check );
				$this->sync_facetwp_with_acf( $facet_field, $new_subfield );
			} else {
				// Get the existing subfield and loop through all of the facet's tags
				$existing_subfield = $facet_field['sub_fields'][ $check ];
				foreach ( $facet['tags'] as $name => $label ) {
					// If a choice is missing, add it to the subfield's choices
					if ( isset( $existing_subfield['choices'] ) && ! $this->check_field_for_choice( $existing_subfield['choices'], $label ) ) {
						$existing_subfield['choices'][ $name ] = $label;
					}
				}

				$this->update_acf_parent_and_child_fields( $group, $facet_field, $existing_subfield, $check );
				$this->sync_facetwp_with_acf( $facet_field, $existing_subfield );
			}
		}
	}

	/**
	 * Updates the subfield at a given index and bubbles up that update to the parent field
	 * to ensure that the group is saved with the new/updated subfield as a known child.
	 * If the index is negative, the subfield is added to the subfield list.
	 *
	 * @param array $group              ACF field group containing the facet fields.
	 * @param array $field              The group field that holds the facet subfields
	 * @param array $subfield           The subfield being updated or created
	 * @param int   $subfield_index       The index of the subfield in the subfield list
	 *
	 * @return void
	 */
	public function update_acf_parent_and_child_fields( $group, $field, $subfield, $subfield_index ) {
		if ( isset( $subfield['choices'] ) ) {
			$subfield['choices'] = $this->ensure_ordered_tags( $subfield['choices'] );
			// $updated_subfield = acf_update_field($subfield);
			// $updated_subfield = update_field($subfield['key'], $subfield['choices']);

			if ( $subfield_index < 0 ) {
				$field['sub_fields'][] = $subfield;
			} else {
				$field['sub_fields'][ $subfield_index ] = $subfield;
			}
			// $updated_field = update_field($field['key'], $field['sub_fields']);
			if ( isset( $group['key'] ) ) {
				$g_fields = acf_get_fields( $group['key'] );
				if ( 'facets' === $g_fields[15]['name'] ) {
					if ( function_exists( 'acf_update_field' ) ) {
						$updated_field = acf_update_field( $field );
					} else if ( $this->cli ) {
						WP_CLI::line( '[ERROR] Unable to call acf_update_field()...' );
					}
				}
			} else if ( $this->cli ) {
				WP_CLI::line( '[ERROR] Group fields could not be found...' );
			}
		} else if ( $this->cli ) {
			WP_CLI::line( '[ERROR] Subfield choices were undefined for field: ' . $field );
		}
	}

	/**
	 * Gets the non-empty facets from a given product. Facet existence is confirmed by the presence
	 * of a facet description. If no facet tags exist, it will hold an empty string.
	 *
	 * @param Object $product               A product taken from the .xml file.
	 * @param string $post_id               The associated WordPress post ID
	 *
	 * @return array                        An associative array with facets and their associated tags
	 */
	public function get_facets_from_product( $product, $post_id ) {
		$product_facets = array();
		$i = 1;

		do {
			$cur_facet = 'Facet' . $i;
			$cur_facet_description = $cur_facet . 'Description';
			$tags = null;
			$label = null;
			$continue = false;

			$facets = get_object_vars( $product->$cur_facet_description );
			if ( is_array( $facets ) && 0 < count( $facets ) ) {
				foreach ( $facets as $fd_name => $fd_value ) {
					$label = $fd_value;
					$continue = true;

					$facet_tags = get_object_vars( $product->$cur_facet );
					if ( is_array( $facet_tags ) && 0 < count( $facet_tags ) ) {
						foreach ( get_object_vars( $product->$cur_facet ) as $f_name => $f_value ) {
							$tags = $f_value;
						}

						$product_facets[] = array(
							'label' => $this->combine_similar_terms( $this->format_source_file_spacing( $label ) ),
							'tags' => ! empty( $tags ) ? $this->format_tags_as_choices( explode( ',', $tags ) ) : array(),
						);
					}
				}

				$i++;
			} else if ( $this->cli && empty( $product_facets ) ) {
				WP_CLI::line( '[ERROR] No facets detected on product, aborting...' );
			}
		} while ( $continue );

		return $product_facets;
	}

	/**
	 * Formats the given tags as an associative array with proper formatting for the name and label.
	 *
	 * @param array $tags               The array of tags gotten from a product's facets
	 *
	 * @return array                    Returns the formatted associative array
	 */
	public function format_tags_as_choices( $tags ) {
		$formatted_tags = array();
		if ( ! is_null( $tags ) && ! empty( $tags ) ) {
			foreach ( $tags as $tag ) {
				$source_tag = $this->format_source_file_spacing( $tag );
				$source_formatted_tag = $this->format_acf_label_name( $source_tag );
				if ( ! empty( $source_formatted_tag ) ) {
					$formatted_tags[ $source_formatted_tag ] = $source_tag;
				}
			}
		}

		return $formatted_tags;
	}

	/**
	 * Retrieves a field group from ACF given a group name
	 *
	 * @param string $group_name        The name of the group being retrieved
	 *
	 * @return array                    The field group that was specified, return null if group is not found
	 */
	public function get_field_group_from_acf( $group_name ) {

		$groups = acf_get_field_groups();
		$target_group = null;
		foreach ( $groups as $group ) {
			if ( $group['title'] === $group_name ) {
				$target_group = $group;
				break;
			}
		}

		return $target_group;
	}

	/**
	 * Retrieves a field from a given ACF field group by field name
	 *
	 * @param string $field_name        The name of the field being retrieved
	 * @param array  $group              The ACF field group where the field is being searched for
	 *
	 * @return array                    Returns the field or null if it is not found
	 */
	public function get_field_from_acf_group( $field_name, $group ) {

		$target_field = null;
		if ( ! is_null( $group ) ) {

			$fields = acf_get_fields( $group['key'] );
			foreach ( $fields as $field ) {
				if ( $field['name'] === $field_name ) {
					$target_field = $field;
					break;
				}
			}
		}

		return $target_field;
	}

	/**
	 * Checks the existing Facets sub-fields for duplicates
	 *
	 * @param array  $existing_fields    Array of fields in the Facets sub-fields
	 * @param string $field_name        The name of the field being checked-for
	 *
	 * @return int                      -1 for missing field, otherwise return index
	 */
	public function find_facet_field_in_acf_subfields( $existing_fields, $field_name ) {

		$subfield_i = ( -1 );
		foreach ( $existing_fields as $index => $e_field ) {
			if ( $e_field['label'] == $field_name ) {
				$subfield_i = $index;
				break;
			}
		}

		return $subfield_i;
	}

	/**
	 * Checks an existing field to see if the choices being updated already exist
	 *
	 * @param array  $existing_choices   Array of choices from existing acf field
	 * @param string $choice            The choice being checked
	 *
	 * @return bool                     Return true if the field is found, false otherwise
	 */
	public function check_field_for_choice( $existing_choices, $choice ) {

		$choice_match = false;
		foreach ( $existing_choices as $name => $label ) {
			if ( $label == $choice ) {
				$choice_match = true;
				break;
			}
		}

		return $choice_match;
	}

	/**
	 * Searches for an existing subfield within a specified ACF field group and returns its index.
	 *
	 * This function iterates through the subfields of a given ACF facet field to find a subfield whose name matches
	 * the specified field name. If a match is found, it returns the index of the subfield within the array of subfields.
	 * If no match is found, it returns -1, indicating that the subfield does not exist.
	 *
	 * @param string $field_name The name of the subfield to search for.
	 * @param array  $group The ACF field group containing the facet field.
	 * @param array  $facet_field The specific ACF facet field containing subfields to be searched.
	 * @return int The index of the subfield if found, otherwise -1.
	 */
	public function check_for_existing_subfield_name_in_acf( $field_name, $group, $facet_field ) {
		foreach ( $facet_field['sub_fields'] as $index => $sub_field ) {
			if ( $sub_field['name'] == $field_name ) {
				return $index;
			}
		}

		return ( -1 );
	}

	/**
	 * Creates a new acf subfield to represent a FacetWP facet.
	 *
	 * @param string $field_label       The Facet Description
	 * @param array  $field_choices      The Facet Tags
	 * @param int    $parent_id            The ID of the parent field
	 *
	 * @return array                    Returns the newly-created field or null if field name already exists
	 */
	public function format_new_acf_facet_field( $field_label, $field_choices, $parent_id ) {

		$field_name = $this->format_acf_label_name( $field_label );

		$new_field = array(
			'key' => 'field_' . uniqid(),
			'label' => $field_label,
			'name' => $field_name,
			'type' => 'checkbox',
			'parent' => $parent_id,
			'choices' => $field_choices,
		);

		return $new_field;
	}

	/**
	 * Checks if there is an existing Facet that corresponds with the ACF subfield. If
	 * there isn't one, it is created and the FacetWP settings are updated.
	 *
	 * @param array $field              The parent group field that holds the subfields
	 * @param array $subfield           The subfield that holds facet name and tags
	 */
	public function sync_facetwp_with_acf( $field, $subfield ) {
		if ( isset( $subfield['name'] ) && isset( $subfield['label'] ) && isset( $subfield['key'] ) && isset( $field['key'] ) ) {
			$fwp = \FacetWP::instance();
			$option = get_option( 'facetwp_settings' );
			$settings = ( false !== $option ) ? json_decode( $option, true ) : [];
			$name = 'pf_' . $subfield['name'];

			if ( $this->cli ) {
				WP_CLI::line( 'Syncing ' . trim( $name ) . ' with ACF...' );
			}
			// If the facet doesn't currently exist, create it
			if ( ! $fwp->helper->get_facet_by_name( $name ) ) {

				$new_facet = array(
					'name' => 'pf_' . $subfield['name'],
					'label' => $subfield['label'],
					'type' => 'checkboxes',
					'source' => 'acf/' . $field['key'] . '/' . $subfield['key'],
					'count' => -1,
					'soft_limit' => 5,
				);

				$settings['facets'][] = $new_facet;

				if ( $this->cli ) {
					WP_CLI::line( 'Creating new Facet: ' . $new_facet['name'] );
				}
				update_option( 'facetwp_settings', json_encode( $settings ), 'no' );

			}
		} else if ( $this->cli ) {
			WP_CLI::line( '[ERROR] Malformed field and subfield data when trying to sync facet...' );
		}
	}

	/**
	 * Formats a string in acf label format ( 'Label Name' => 'label_name' ).
	 *
	 * @param string $name              The string to be formatted
	 *
	 * @return string                   The formatted string
	 */
	public function format_acf_label_name( $name ) {
		$formatted_str = sanitize_title( trim( $name ) );
		$formatted_str = $this->trim_illegal_chars_from_str( $formatted_str );
		$formatted_str = str_replace( ' ', '_', strtolower( $formatted_str ) );
		$formatted_str = str_replace( '-', '_', $formatted_str );
		return $formatted_str;
	}

	/**
	 * Removes any bracket character from the provided string.
	 *
	 * @param string $str               The string to be modified
	 *
	 * @return string                   The modified string
	 */
	public function trim_illegal_chars_from_str( $str ) {

		$new_str = $str;
		$charset = '(){}[]<>!@#$%^&*/\\';
		$chars = str_split( $charset );
		foreach ( $chars as $char ) {
			$new_str = str_replace( $char, '', $new_str );
		}

		// Trim any extra leftover whitespace (2 or 3 spaces should be max)
		$new_str = str_replace( '   ', ' ', $new_str );
		$new_str = str_replace( '  ', ' ', $new_str );

		// Just in case, trim the ends
		$new_str = trim( $new_str );

		return $new_str;
	}

	/**
	 * Checks and combines various terms via regular expressions.
	 *
	 * @param string $term              The string being checked
	 *
	 * @return string                   Return the modified string or the unchanged string
	 */
	public function combine_similar_terms( $term ) {

		$control_monitoring_pattern = '/Control(s)*((\s)|(\s(and|&)*\s))Monitoring/i';
		$system_type_pattern = '/System\sType/i';
		$input_voltage_pattern = '/Input Voltage/i';
		$output_voltage_pattern = '/Output Voltage/i';

		// Combine Controls and Monitoring Type variants
		if ( preg_match( $control_monitoring_pattern, $term ) ) {
			return 'Controls and Monitoring Type';
		}

		// Merge System Type into Product Type
		if ( preg_match( $system_type_pattern, $term ) ) {
			return 'Product Type';
		}

		// Input Voltage
		if ( preg_match( $input_voltage_pattern, $term ) ) {
			return 'Input Voltage';
		}

		// Output Voltage
		if ( preg_match( $output_voltage_pattern, $term ) ) {
			return 'Output Voltage';
		}

		// Capactiy (amps)
		if ( 'Capacity' === $term ) {
			return 'Capacity (amps)';
		}

		return $term;
	}

	/**
	 * Formats a string by adding spaces inbetween unseparated words (remove camelCase).
	 * Checks for acronyms to avoid splitting them apart.
	 *
	 * @param string $string            The string to be formatted
	 *
	 * @return string                   The formatted string
	 */
	public function format_source_file_spacing( $string ) {

		$chars = str_split( $string );
		$prev_char = null;
		$next_char = null;
		$formatted_string = '';

		foreach ( $chars as $index => $char ) {

			$next_char = $chars[ $index + 1 ] ?? null;

			// If the current char is uppercase and not the first or last char
			if ( ctype_upper( $char ) && ! is_null( $prev_char ) && ! is_null( $next_char ) ) {
				// Check if the pattern is lowercase -> uppercase -> lowercase
				if ( ctype_lower( $prev_char ) && ctype_lower( $next_char ) ) {
					$formatted_string .= ' ';
				}
			}

			$formatted_string .= $char;
			$prev_char = $char;
		}

		return $formatted_string;
	}

	/**
	 * Reorders an array of tags into alphabetical order.
	 *
	 * @param array $tags               An associative array of facet tags
	 *
	 * @return array                    Returns the reordered array
	 */
	public function ensure_ordered_tags( $tags ) {

		$new_tags = $tags;
		if ( is_array( $new_tags ) ) {
			sort( $new_tags );
			$new_tags = $this->format_tags_as_choices( $new_tags );
		}

		return $new_tags;
	}

	/**
	 * Updates the subfields of a post based on its associated XML data.
	 *
	 * @param SimpleXMLElement $value XML data containing product information.
	 * @param int              $post_id ID of the post to update.
	 */
	public function update_sub_fields( $value, $post_id ) {

		/**
		 *  PRODUCT CUSTOM FIELD DOCUMENTATION
		 *  Parent Product ID
		 *  parent_product_id
		 *   Text
		 *   2
		 *   Product ID
		 *   product_id
		 *   Text
		 *   3
		 *   Sub Products
		 *   sub_products
		 *   Text
		 *   4
		 *   Product Group
		 *   product_group
		 *   Text
		 *   5
		 *   Product Category
		 *   product_category
		 *   Text
		 *   6
		 *   Product Features
		 *   product_features
		 *   Text Area
		 *   7
		 *   Product Benefits
		 *   product_benefits
		 *   Text Area
		 *   8
		 *   Product Images
		 *   product_images
		 *   Repeater
		 *   9
		 *   Product Documents
		 *   product_documents
		 *   Repeater
		 *   10
		 *   Product Videos
		 *   product_videos
		 *   Repeater
		 *   11
		 *   Created Date
		 *   created_date
		 *   Text
		 *   12
		 *   Modified Date
		 *   modified_date
		 *   Text
		 *   13
		 *   Published Date
		 *   published_date
		 *   Text
		 *   14
		 *   Regions Sold In
		 *   regions_sold_in
		 *   Text
		 *   15
		 *   Countries Sold In
		 *   countries_sold_in
		 *   Text
		 *   16
		 *   Facets
		 *   facets
		 *   Group [checkboxes]
		 */
		$this->update_text_field( $value->Product->ParentProductId, 'parent_product_id', $post_id );
		$this->update_text_field( $value->Product->ProductId, 'product_id', $post_id );
		$this->update_text_field( $value->Product->SubProducts, 'sub_products', $post_id );
		$this->update_text_field( $value->Product->ProductGroup, 'product_group', $post_id );
		$this->update_text_field( $value->Product->ProductCategory, 'product_category', $post_id );
		$this->update_text_field( $value->Product->ProductFeatures, 'product_features', $post_id );
		$this->update_text_field( $value->Product->ProductBenefits, 'product_benefits', $post_id );
		// TODO: Abstract these into one function for repeaters
		if ( isset( $value->Product->ProductAssets->ProductImages ) ) {
			$this->update_product_images( $value, $post_id );
		}
		if ( isset( $value->Product->ProductAssets->ProductDocuments ) ) {
			$this->update_product_documents( $value, $post_id );
		}
		if ( isset( $value->Product->ProductAssets->ProductVideos ) ) {
			$this->update_product_videos( $value, $post_id );
		}
		$this->update_text_field( $value->Product->CreatedDate, 'created_date', $post_id );
		$this->update_text_field( $value->Product->ModifiedDate, 'modified_date', $post_id );
		$this->update_text_field( $value->Product->PublishedDate, 'published_date', $post_id );
		$this->update_text_field( $value->Product->RegionsSoldIn, 'regions_sold_in', $post_id );
		$this->update_text_field( $value->Product->CountriesSoldIn, 'countries_sold_in', $post_id );
		$this->update_product_facets( $value->Product, 'facets', $post_id );
	}

	/**
	 * Updates the ACF field for product facets based on the provided product data.
	 *
	 * This function retrieves facets from a product, formats them, and updates the corresponding ACF field in the WordPress database
	 * for the specified post. It associates each facet with its corresponding tags.
	 *
	 * @param object $product The product object containing facet data.
	 * @param string $key The ACF field key where the facets will be stored.
	 * @param int    $post_id The ID of the WordPress post associated with the product to update.
	 */
	public function update_product_facets( $product, $key, $post_id ) {

		$product_facets = $this->get_facets_from_product( $product, $post_id );

		$facets = array();
		foreach ( $product_facets as $facet ) {
			$keyed_array = array_keys( $facet['tags'] );
			$facets[ $this->format_acf_label_name( $facet['label'] ) ] = $keyed_array;
		}

		update_field( $key, $facets, $post_id );
	}

	/**
	 * Updates a specific text field within a post's ACF fields.
	 *
	 * This function updates a given ACF field with a string value, ensuring the value is properly cast to a string before updating.
	 *
	 * @param mixed  $value The value to be stored in the field.
	 * @param string $key The ACF field key to update.
	 * @param int    $post_id The ID of the WordPress post where the ACF field is located.
	 */
	public function update_text_field( $value, $key, $post_id ) {
		if ( isset( $value ) ) {
			update_field( $key, (string) $value, $post_id );
		}
	}

	/**
	 * Updates and saves product images to a specified directory and updates the corresponding ACF field.
	 *
	 * This function iterates over product images, processes each image URL, retrieves image data, determines the appropriate
	 * file extension, and saves the image to a designated directory. It then updates an ACF field with the image details.
	 *
	 * @param object $value The product object containing image data.
	 * @param int    $post_id The ID of the WordPress post associated with the product to update.
	 */
	public function update_product_images( $value, $post_id ) {
		$i = 0;
		$images = array();

		$upload_dir = wp_upload_dir();
		$document_directory = $upload_dir['basedir'] . '/products';

		if ( ! file_exists( $document_directory ) ) {
			wp_mkdir_p( $document_directory );
		}

		foreach ( $value->Product->ProductAssets->ProductImages->ProductImages as $key => $value ) {

			if ( ! empty( $value['url'] ) ) {

				// XML feed does not have file extensions on images. This is necessary to out correctly on the frontend.
				// This is why we aren't using basename()
				$src = (string) $value['url'];
				$group = (string) $value['groupName'];

				$file = getimagesize( $src );
				$imagetype = '.notfound';

				if ( $file ) {
					if ( 'image/png' == $file['mime'] ) {
						$imagetype = '.png';
					} elseif ( 'image/jpeg' == $file['mime'] || 'image/jpg' == $file['mime'] ) {
						$imagetype = '.jpg';
					}
					$file_name = basename( $src ) . $imagetype;

					// TODO: Add check to not always download images
					// if (strpos($file_name, '') == false) {}

					$images[ $i ]['image'] = $file_name;
					$images[ $i ]['group'] = $group;
					file_put_contents( $document_directory . '/' . $file_name, file_get_contents( $src ) );
					$i++;
				}
			}
		}
		update_field( 'product_images', $images, $post_id );
	}


	/**
	 * Updates the video metadata for a given product post based on the provided product data.
	 *
	 * This function iterates over an array of product videos, extracting URLs from the data and formatting it for
	 * storage. It then updates the corresponding custom field ('product_videos') in the WordPress database for the
	 * specified post, effectively storing the URLs of the videos associated with the product.
	 *
	 * @param SimpleXMLElement $value XML element containing product video details.
	 * @param int              $post_id The ID of the WordPress post associated with the product to update.
	 */
	public function update_product_videos( $value, $post_id ) {
		$i = 0;
		$videos = array();
		foreach ( $value->Product->ProductAssets->ProductVideos->ProductVideos as $key => $value ) {
			if ( ! empty( $value['fullStreamingUrl'] ) ) {
				$videos[ $i ]['video'] = (string) $value['fullStreamingUrl'];
				$i++;
			}
		}
		update_field( 'product_videos', $videos, $post_id );
	}


	/**
	 * Updates the document metadata for a given product post based on the provided product data.
	 *
	 * This function iterates over an array of product documents, extracting and formatting document data such as
	 * titles, group names, and URLs, then updates the corresponding custom field in the WordPress database for the
	 * specified post.
	 *
	 * @param SimpleXMLElement $value XML element containing product document details.
	 * @param int              $post_id The ID of the WordPress post associated with the product to update.
	 */
	public function update_product_documents( $value, $post_id ) {

		$i = 0;
		$documents = array();
		foreach ( $value->Product->ProductAssets->ProductDocuments->Document as $key => $value ) {
			if ( ! empty( $value['url'] ) ) {
				$documents[ $i ]['title'] = (string) $value['documentTitle'];
				$documents[ $i ]['group'] = (string) $value['GroupName'];
				$documents[ $i ]['document'] = (string) $value['url'];
				$i++;
			}
		}
		update_field( 'product_documents', $documents, $post_id );
	}


	/**
	 *
	 * Gets all products out of the xml files in the /app/uploads/xml folder. Puts them into an array.
	 *
	 * @return  array  Array of SimpleXML Objects that represent Products in the XML feed.
	 */
	public function get_all_xml_products() {

		$products = array();
		$ids = array();

		$xml_files = $this->get_xml_files();

		foreach ( $xml_files as $xml_file ) {

			$xml_info = simplexml_load_file( $xml_file );

			foreach ( $xml_info->Product as $key => $value ) {

				$correct_region = $this->is_product_sold_in_region( $value->Product, 'North America NA', 'USA' );

				$correct_group = $this->is_product_in_correct_category( $value->Product );

				if ( $correct_region && $correct_group ) {
					$id = (string) $value->Product->ProductId;
					$ids[] = $id;
					$products[] = $value;
				}
			}
		}

		$this->update_product_ids_in_database( $ids );

		return $products;
	}

	/**
	 * Processes an array of products to categorize them into 'create', 'update', or 'malformed' groups based on certain criteria.
	 *
	 * This function checks each product's publication and modification dates against a specified date range and determines
	 * whether each product needs to be created anew, updated, or marked as malformed due to missing or invalid data.
	 *
	 * @param array  $products Array of product objects to be processed.
	 * @param string $date Optional. A date string that sets the lower bound for considering product updates. Default is '-1 year'.
	 * @return array Returns an associative array with three keys:
	 *               - 'update' => array of products that exist in the database and need updating.
	 *               - 'create' => array of new products that need to be created.
	 *               - 'malformed' => array of products that do not meet the required data standards.
	 */
	public function package_products( $products, $date = '-1 year' ) {
		$prod_arr = [
			'update'    => [],
			'create'    => [],
			'malformed' => [],
		];

		if ( is_null( $this->db_products ) ) {
			$this->db_products = $this->get_products_from_database();
		}

		foreach ( $products as $product ) {
			$id = (string) $product->Product->ProductId;
			if ( ! is_null( $date ) && false !== strtotime( $date ) ) {
				$date_check = strtotime( gmdate( 'Y-m-d', strtotime( $date ) ) );
				$pub_date = strtotime( substr( $product->Product->PublishedDate, 0, 10 ) );
				$created_date = strtotime( substr( $product->Product->CreatedDate, 0, 10 ) );
				$mod_date = strtotime( substr( $product->Product->ModifiedDate, 0, 10 ) );
				if ( $pub_date < $date_check && $created_date < $date_check && $mod_date < $date_check ) {
					continue;
				}
			}

			if ( ! empty( $id ) ) {
				$existing_product = $this->check_if_product_exists( $id, $this->db_products );
				if ( is_object( $existing_product ) ) {
					$prod_arr['update'][] = [
						'id'    => $existing_product->ID,
						'pid'   => $existing_product->product_id,
						'name'  => $existing_product->post_title,
						'data'  => $product,
					];
				} else {
					$prod_name = (string) $product->Product->ProductName;
					if ( ! empty( $prod_name ) ) {
						$prod_arr['create'][] = [
							'pid'   => $id,
							'name'  => $prod_name,
							'data'  => $product,
						];
					} else {
						$prod_arr['malformed'][] = [
							'pid'   => $id,
							'name'  => null,
							'data'  => $product,
						];
					}
				}
			}
		}

		if ( $this->cli ) {
			WP_CLI::line( 'Products to Create: ' . count( $prod_arr['create'] ) );
			WP_CLI::line( 'Products to Update: ' . count( $prod_arr['update'] ) );
			WP_CLI::line( 'Malformed Products: ' . count( $prod_arr['malformed'] ) );
		}

		return $prod_arr;
	}

	/**
	 * Updates or creates a database option to track the IDs of products that have been imported from the feed.
	 *
	 * TODO: Create database option that tracks products that have been in the feed and have been deleted. (blacklisted)
	 *
	 * This function checks if a WordPress option exists to store imported product IDs. If it exists and the given
	 * product ID is not already in the list, the ID is added. If the option does not exist, it is created with the
	 * given product ID as its initial value.
	 *
	 * @param int|string $id The product ID to add to the database tracking list.
	 */
	public function update_product_id_database( $id ) {

		if ( get_option( 'vertiv_product_ids' ) !== false ) {
			$product_ids = get_option( 'vertiv_product_ids' );
			if ( ! in_array( $id, $product_ids, false ) ) {
				array_push( $product_ids, $id );
				update_option( 'vertiv_product_ids', $product_ids );
			}
		} else {
			$product_ids = array( $id );
			add_option( 'vertiv_product_ids', $product_ids );
		}
	}

	/**
	 * Takes an array of product IDs, sorts and cleans it, and then updates the database with the array.
	 *
	 * @param array $product_ids        The array of product IDs
	 */
	public function update_product_ids_in_database( $product_ids ) {
		$product_ids = $this->remove_array_duplicates( $product_ids );
		update_option( 'vertiv_product_ids', $product_ids );
	}

	/**
	 * Sorts and cleans an array of duplicate values. All array values must be of the same type.
	 *
	 * @param array $array              The array being cleaned
	 *
	 * @return array                    The clean array
	 */
	public function remove_array_duplicates( $array ) {

		sort( $array );
		$clean_array = array();

		$prev = null;

		foreach ( $array as $index => $value ) {
			if ( $value != $prev ) {
				$clean_array[] = $value;
				$prev = $value;
			}
		}

		return $clean_array;
	}

	/**
	 * Checks if a given product ID is already stored in the database option tracking imported products.
	 *
	 * This function retrieves an option containing a list of product IDs that have been imported. It checks if the
	 * specified product ID is in that list, returning true if found and false otherwise. If the option does not
	 * exist, it returns false.
	 *
	 * @param int|string $id The product ID to check against the stored list of imported product IDs.
	 * @return bool Returns true if the product ID is found in the database, false otherwise.
	 */
	public function check_product_database( $id ) {

		if ( false !== get_option( 'vertiv_product_ids' ) ) {
			$product_ids = get_option( 'vertiv_product_ids' );
			if ( in_array( $id, $product_ids, true ) ) {
				return true;
			} else {
				return false;
			}
		}

		return false;
	}

	/**
	 * Executes a weekly cron job that updates product data, replaces old XML files, and sends out a product update report via email.
	 *
	 * @param int $time Optional. Modifier to adjust the timeframe for the cron job. Default is -1, which sets the date range to -1 year.
	 * @return void
	 */
	public function cron_job_report( $time = ( -1 ) ) {
		$date = '-1 year';

		wp_defer_comment_counting( true );
		wp_defer_term_counting( true );

		// Run the download prior to other operations only if we aren't running via site request
		if ( empty( $_POST['action'] ) ) {
			if ( $this->cli ) {
				WP_CLI::line( 'Running download prior to other operations...' );
			}
			$this->import_vertiv_products();
		}

		// Set the date range
		if ( $this->cli && $time >= 0 ) {
			$date = '-' . $time . ' day';
		}

		if ( $this->cli ) {
			WP_CLI::line( 'Timeframe Set: ' . $date );
		}

		$products = $this->get_all_xml_products();

		if ( $this->cli ) {
			WP_CLI::line( 'Products retrieved... Starting report...' );
		}

		$this->send_product_report( $products, $date );

		if ( $this->cli ) {
			WP_CLI::line( 'Product Report Complete... Re-Saving Products...' );
		}

		if ( is_null( $this->db_products ) ) {
			$this->db_products = $this->get_products_from_database();
		}

		wp_defer_comment_counting( false );
		wp_defer_term_counting( false );

		// TODO: Fix the Facet indexer; it doesn't work for some reason
		// if ($this->cli) {
		// WP_CLI::line('Re-Indexing FacetWP...');
		// }
		// $this->clean_cache_wp_rocket();
		// $fwp = \FacetWP::instance();
		// $fwp->indexer->index();

		// Exits admin_post action when manually running the cron from the admin page.
		// TODO: Abstract this as a function die_properly with parameter 'action' or something
		if ( ! empty( $_POST['action'] ) && 'cron_job_report' === $_POST['action'] ) {
			$redirect_url = get_site_url() . '/wp-admin/admin.php?page=cf_tools';
			wp_safe_redirect( $redirect_url );
			exit;
		}
	}

	/**
	 * Logs strategy data for debugging purposes, handling nested data structures recursively.
	 *
	 * @param string $key The key or name of the data being logged.
	 * @param mixed  $value The value of the data being logged, which can be an array or object.
	 * @param int    $depth Current depth of the recursion, used for formatting the output. Default is 0.
	 */
	public function log_strategy( $key, $value, $depth = 0 ) {
		if ( $this->cli ) {
			$indent = '';
			if ( 0 < $depth ) {
				for ( $i = 0; $i < $depth; $i++ ) {
					$indent .= '  ';
				}
			}
			if ( is_object( $value ) || is_array( $value ) ) {
				WP_CLI::line( $indent . $key . ' : ' );
				foreach ( $value as $key2 => $value2 ) {
					$this->log_strategy( $key2, $value2, $depth + 1 );
				}
			} else {
				WP_CLI::line( $indent . $key . ' : ' . $value );
			}
		}
	}

	/**
	 * Sends an email report about product updates.
	 *
	 * @param array  $products Products data to be reported.
	 * @param string $date The date range for which the report is compiled. Default is '-1 week'.
	 */
	public function send_product_report( $products, $date = '-1 year' ) {

		$packed_products = $this->package_products( $products, $date );
		if ( isset( $packed_products['create'] ) ) {
			foreach ( $packed_products['create'] as $product_to_create ) {
				if ( isset( $product_to_create['data'] ) ) {
					$this->create_product_posts( $product_to_create['data'] );
				}
			}
		}
		if ( isset( $packed_products['update'] ) ) {
			foreach ( $packed_products['update'] as $product_to_update ) {
				if ( isset( $product_to_update['data'] ) && isset( $product_to_update['id'] ) ) {
					$this->update_product_posts( $product_to_update['data'], $product_to_update['id'] );
				}
			}
		}

		$report = $this->create_product_report( $packed_products );

		wp_mail( $report['to'], $report['subject'], $report['message'], $report['headers'] );
	}

	/**
	 * Creates an email report from given product data, formatting the content as HTML.
	 *
	 * @param array $products Products data to be included in the report.
	 * @return array Returns an array with email headers, recipient, subject, and message.
	 */
	public function create_product_report( $products ) {

		$headers = 'Content-type: text/html; charset=iso-8859-1' . "\r\n";
		$headers .= 'From: Product Import Report <no-reply@commonframework.us>' . "\r\n";
		if ( 'production' === WP_ENV ) {
			// Send to everyone for production reports
			$to = 'websupport@strategynewmedia.com, jason.wilson@strategynewmedia.com, josh.proctor@strategynewmedia.com';
			$subject = 'Product Import Report';
		} else {
			// Send only to websupport for development reports
			$to = 'developers@strategynewmedia.com';
			$subject = 'Product Import Report (DEVELOPMENT)';
		}
		// UNCOMMENT THIS FOR TROUBLESHOOTING
		// $to = 'developers@strategynewmedia.com';
		// $subject = 'Product Import Report (TROUBLESHOOTING)';
		$message = '';

		write_log( 'Create Products: ' . count( $products['create'] ) );
		write_log( 'Update Products: ' . count( $products['update'] ) );
		write_log( 'Malformed Products: ' . count( $products['malformed'] ) );

		if ( isset( $products['create'] ) && ! empty( $products['create'] ) ) {

			$message .= '<h2>This is a list of products that need to be created.</h2>';
			$message .= '<table>';
			$message .= '<tr>';
			$message .= '<th>Product ID</th>';
			$message .= '<th>Product Name</th>';
			$message .= '</tr>';

			foreach ( $products['create'] as $product ) {
				$message .= '<tr>';
				$message .= '<td>' . $product['pid'] . '</td>';
				$message .= '<td>' . $product['name'] . '</td>';
				$message .= '</tr>';
			}

			$message .= '</table>';
		} else {
			$message .= '<h2>No products have been created.</h2>';
		}

		if ( isset( $products['update'] ) && ! empty( $products['update'] ) ) {

			$message .= '<h2>This is a list of updated products.</h2>';
			$message .= '<table>';
			$message .= '<tr>';
			$message .= '<th>Product ID</th>';
			$message .= '<th>Product Name</th>';
			$message .= '</tr>';

			foreach ( $products['update'] as $product ) {
				$message .= '<tr>';
				$message .= '<td>' . $product['pid'] . '</td>';
				$message .= '<td>' . $product['name'] . '</td>';
				$message .= '</tr>';
			}

			$message .= '</table><br>';
		} else {
			$message .= '<h2>No available products need to be updated.</h2>';
		}

		if ( isset( $products['malformed'] ) && ! empty( $products['malformed'] ) ) {

			$message .= '<h2>This is a list of malformed products.</h2>';
			$message .= '<table>';
			$message .= '<tr>';
			$message .= '<th>Product ID</th>';
			$message .= '</tr>';

			foreach ( $products['malformed'] as $product ) {
				$message .= '<tr>';
				$message .= '<td>' . $product['pid'] . '</td>';
				$message .= '</tr>';
			}

			$message .= '</table><br>';
		} else {
			$message .= '<h2>There were no malformed products found.</h2>';
		}

		return array(
			'headers'   => $headers,
			'to'        => $to,
			'subject'   => $subject,
			'message'   => $message,
		);
	}

	/**
	 * Registers FacetWP templates by loading them from JSON files.
	 *
	 * @param array $templates Existing array of templates.
	 * @return array Modified array of templates including any new ones added from the files.
	 */
	public function register_facet_templates( $templates ) {
		if ( file_exists( COMMON_FRAMEWORK_PLUGIN_PATH . 'templates/facet-listings/product-filter.json' ) ) {
			$file_contents = file_get_contents( COMMON_FRAMEWORK_PLUGIN_PATH . 'templates/facet-listings/product-filter.json' );
			if ( $file_contents ) {
				$templates[] = json_decode( $file_contents, true );
			}
		}

		return $templates;
	}

	/**
	 * Broadcasts FacetWP facets across a multisite network by loading additional facet data from files.
	 *
	 * @param array $facets Current array of facets.
	 * @return array Modified array of facets including any new ones added from the files.
	 */
	public function broadcast_facets( $facets ) {
		if ( 'https://commonframework.us' !== home_url() && file_exists( COMMON_FRAMEWORK_PLUGIN_PATH . 'templates/facets' ) ) {
			$templates = array_diff( scandir( COMMON_FRAMEWORK_PLUGIN_PATH . 'templates/facets' ), array( '.', '..' ) );
			if ( 0 < count( $templates ) ) {
				foreach ( $templates as $template ) {
					$file_contents = file_get_contents( COMMON_FRAMEWORK_PLUGIN_PATH . 'templates/facets/' . $template );
					if ( $file_contents ) {
						$facet_data = json_decode( $file_contents, true );
						$facets[] = $facet_data;
					}
				}
			}
		}

		return $facets;
	}

	/**
	 * Filters and modifies facet indexer parameters to avoid indexing certain values based on predefined patterns.
	 *
	 * @param array  $params Parameters including the facet's display value and name.
	 * @param object $class The instance of the class that is invoking the filter.
	 * @return array Modified parameters for the facet indexer.
	 */
	public function filter_facet_indexer( $params, $class ) {
		$facet_pattern = '/pf_/i';
		$num_pattern = '~[0-9]+~';
		if ( ! isset( $params['facet_display_value'] ) || is_null( $params['facet_display_value'] ) ||
			 ! isset( $params['facet_name'] ) || is_null( $params['facet_name'] )
		) {
			return $params;
		}
		if ( ( preg_match( $facet_pattern, $params['facet_name'] ) && false !== strpos( $params['facet_display_value'], '_' ) ) ||
			 ( strtolower( $params['facet_display_value'] ) == $params['facet_display_value'] && ! preg_match( $num_pattern, $params['facet_display_value'] ) )
		) {
			$params['facet_value'] = '';
		}

		return $params;
	}
}
