<?php

namespace CommerceBird\Admin\Actions\Ajax;

use CMBIRD_Auth_Zoho;
use CMBIRD_API_Handler_Zoho;
use CMBIRD_Pricelist_ZI;
use CMBIRD_Categories_ZI;
use CommerceBird\Admin\Template;
use CommerceBird\Admin\Traits\AjaxRequest;
use CommerceBird\Admin\Traits\OptionStatus;
use CommerceBird\Admin\Traits\Singleton;
use CommerceBird\Admin\Traits\LogWriter;
use Throwable;
use WC_Tax;
use function gettype;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}
final class ZohoInventoryAjax {


	use Singleton;
	use AjaxRequest;
	use OptionStatus;
	use LogWriter;

	private const FORMS = array(
		'tax'     => array(
			'selectedTaxRates',
		),
		'product' => array(
			'item_from_zoho',
			'disable_stock_sync',
			'disable_product_sync',
			'enable_accounting_stock',
		),
		'cron'    => array(
			'form',
			'categories',
		),
		'order'   => array(
			'disable_sync',
			'enable_auto_number',
			'enable_order_status',
			'enable_sales_order_cycle',
			'order_prefix',
			'location_id',
			'enable_locationstock',
			'location_country_map',
		),
		'contact' => array(
			'enable_cron',
		),
		'price'   => array(
			'wp_user_role',
			'zoho_inventory_pricelist',
			'wcb2b',
		),
	);

	private const ACTIONS = array(
		'get_zoho_connect'                        => 'connection_get',
		'save_zoho_connect'                       => 'connection_set',
		'reset_zoho_connect'                      => 'connection_reset',
		'get_zoho_tax'                            => 'tax_get',
		'save_zoho_tax'                           => 'tax_set',
		'reset_zoho_tax'                          => 'tax_reset',
		'get_zoho_product'                        => 'product_get',
		'save_zoho_product'                       => 'product_set',
		'reset_zoho_product'                      => 'product_reset',
		'get_zoho_cron'                           => 'cron_get',
		'save_zoho_cron'                          => 'cron_set',
		'reset_zoho_cron'                         => 'cron_reset',
		'get_zoho_order'                          => 'order_get',
		'save_zoho_order'                         => 'order_set',
		'reset_zoho_order'                        => 'order_reset',
		'get_zoho_contact'                        => 'contact_get',
		'save_zoho_contact'                       => 'contact_set',
		'reset_zoho_contact'                      => 'contact_reset',
		'get_zoho_price'                          => 'price_get',
		'save_zoho_price'                         => 'price_set',
		'reset_zoho_price'                        => 'price_reset',
		'get_zoho_fields'                         => 'fields_get',
		'save_zoho_fields'                        => 'fields_set',
		'reset_zoho_fields'                       => 'fields_reset',
		'is_connected'                            => 'connection_done',
		'get_wc_taxes'                            => 'wc_tax_collect',
		'get_zoho_taxes'                          => 'zoho_tax_rates_collect',
		'get_zoho_categories'                     => 'zoho_categories_collect',
		'get_zoho_locations'                      => 'zoho_locations_collect',
		'get_zoho_prices'                         => 'zoho_prices_collect',
		'get_all_custom_fields'                   => 'wc_custom_fields_collect',
		'get_zoho_custom_fields'                  => 'zoho_custom_fields_collect',
		'get_zoho_webhooks'                       => 'get_zoho_webhooks',
		'handle_code'                             => 'handle_code',
		'convert_guest'                           => 'convert_guest',
		'zoho_ajax_call_variable_item_from_zoho'  => 'sync_variable_items_from_zoho',
		'zoho_ajax_call_item_from_zoho'           => 'sync_simple_items_from_zoho',
		'zoho_ajax_call_item'                     => 'sync_items_to_zoho',
		'import_zoho_contacts'                    => 'sync_contacts_from_zoho',
		'zoho_ajax_call_composite_item_from_zoho' => 'sync_composite_items_from_zoho',
		'zoho_ajax_call_composite_item'           => 'sync_composite_items_to_zoho',
		'get_category_mappings'                   => 'category_mappings_get',
		'get_category_review_queue'               => 'category_review_queue_get',
		'set_manual_category_mapping'             => 'category_mapping_set_manual',
		'resolve_category_review'                 => 'category_review_resolve',
		'delete_category_mapping'                 => 'category_mapping_delete',
	);

	private const SOURCE = 'zoho';

	/**
	 * ZohoInventoryAjax constructor.
	 */
	public function __construct() {
		$this->load_actions();
	}

	/**
	 * Collects custom checkout fields for WooCommerce.
	 *
	 * This function verifies the request and collects custom fields from the
	 * THWCFD_Utils::get_checkout_fields() function if the THWCFD class exists.
	 *
	 * @return void
	 */
	public function wc_custom_fields_collect(): void {
		$this->verify();
		$types         = array( 'billing', 'shipping', 'additional' );
		$all_fields    = array();
		$custom_fields = array();
		// Get all the fields.
		foreach ( $types as $type ) {
			// Skip if an unsupported type.
			if (
				! in_array(
					$type,
					array(
						'billing',
						'shipping',
						'additional',
					),
					true,
				)
			) {
				continue;
			}

			$temp_fields = get_option( 'wc_fields_' . $type );
			if ( false !== $temp_fields ) {
				$all_fields = array_merge( $all_fields, $temp_fields );
			}
		}
		// Loop through each field to see if it is a custom field.
		foreach ( $all_fields as $name => $options ) {
			if ( isset( $options['custom'] ) && $options['custom'] ) {
				$label                  = trim( $options['label'] );
				$custom_fields[ $name ] = empty( $label ) ? __( 'Please set label', 'commercebird' ) : $label;
			}
		}

		$this->response = $custom_fields;

		$this->serve();
	}

	/**
	 * Collects custom Zoho Inventory fields for sales orders or items.
	 *
	 * @return void
	 */
	public function zoho_custom_fields_collect(): void {
		$this->verify();

		$entity = isset( $this->request['entity'] ) ? sanitize_key( $this->request['entity'] ) : 'salesorder';
		if ( ! in_array( $entity, array( 'salesorder', 'item' ), true ) ) {
			$this->errors = array( 'message' => 'Invalid entity type' );
			$this->serve();
			return;
		}

		$zoho_inventory_oid = get_option( 'cmbird_zoho_inventory_oid' );
		$zoho_inventory_url = get_option( 'cmbird_zoho_inventory_url' );

		$url = $zoho_inventory_url . 'inventory/v1/settings/fields?entity=' . $entity . '&organization_id=' . $zoho_inventory_oid;

		$execute_curl_call_handle = new CMBIRD_API_Handler_Zoho();
		$zoho_fields              = array();
		$debug                    = array(
			'urls_tried'    => array(),
			'url_succeeded' => null,
			'response_path' => null,
			'raw_code'      => null,
		);

		$json        = $execute_curl_call_handle->execute_curl_call_get( $url );
		$debug_entry = array(
			'url'      => $url,
			'wp_error' => false,
			'code'     => null,
		);

		if ( is_wp_error( $json ) ) {
			$debug_entry['wp_error'] = true;
			$debug_entry['matched']  = 'none';
			$debug['urls_tried'][]   = $debug_entry;
		} else {
			$debug_entry['code'] = isset( $json->code ) ? $json->code : null;
			$debug['raw_code']   = $debug_entry['code'];

			if ( isset( $json->fields ) && is_array( $json->fields ) ) {
				$zoho_fields            = $json->fields;
				$debug_entry['matched'] = 'json->fields';
				$debug['url_succeeded'] = $url;
				$debug['response_path'] = 'json->fields';
			} elseif ( isset( $json->data->fields ) && is_array( $json->data->fields ) ) {
				$zoho_fields            = $json->data->fields;
				$debug_entry['matched'] = 'json->data->fields';
				$debug['url_succeeded'] = $url;
				$debug['response_path'] = 'json->data->fields';
			} else {
				$debug_entry['matched'] = 'none';
			}

			$debug['urls_tried'][] = $debug_entry;
		}

		$fields = array();
		if ( ! empty( $zoho_fields ) ) {
			foreach ( $zoho_fields as $field ) {
				$fields[] = array(
					'field_name'           => isset( $field->field_name ) ? (string) $field->field_name : '',
					'field_name_formatted' => isset( $field->field_name_formatted ) ? (string) $field->field_name_formatted : '',
					'data_type'            => isset( $field->data_type ) ? (string) $field->data_type : '',
					'is_custom_field'      => ! empty( $field->is_custom_field ),
				);
			}
		}

		$this->response = array(
			'fields' => $fields,
			'_debug' => $debug,
		);
		$this->serve();
	}

	/**
	 * Retrieves the connection details.
	 *
	 * @return void
	 */
	public function connection_get(): void {
		$this->verify();
		$this->response['account_domain']  = get_option( 'cmbird_zoho_inventory_domain', '' );
		$this->response['organization_id'] = get_option( 'cmbird_zoho_inventory_oid', '' );
		$this->response['client_id']       = get_option( 'cmbird_zoho_inventory_cid', '' );
		$this->response['client_secret']   = get_option( 'cmbird_zoho_inventory_cs', '' );
		$this->response['inventory_url']   = get_option( 'cmbird_zoho_inventory_url', '' );
		$this->response['redirect_uri']    = get_option( 'cmbird_authorization_redirect_uri', '' );
		$this->serve();
	}

	/**
	 * Retrieves the fields and serves the response.
	 *
	 * @return void
	 */
	public function fields_get(): void {
		$this->verify();

		$entity = isset( $this->request['entity'] ) ? sanitize_key( $this->request['entity'] ) : 'salesorder';
		if ( ! in_array( $entity, array( 'salesorder', 'item' ), true ) ) {
			$entity = 'salesorder';
		}

		$stored_option = get_option( 'cmbird_wootozoho_custom_fields', array() );
		if ( is_string( $stored_option ) ) {
			$decoded = json_decode( $stored_option, true );
			if ( is_array( $decoded ) ) {
				$stored_option = $decoded;
			}
		}

		if ( is_array( $stored_option ) && ( isset( $stored_option['salesorder'] ) || isset( $stored_option['item'] ) ) ) {
			$this->response['form'] = isset( $stored_option[ $entity ] ) && is_array( $stored_option[ $entity ] ) ? $stored_option[ $entity ] : array();
		} else {
			// Backward compatibility: legacy flat mapping belongs to product/item mapping.
			$this->response['form'] = 'item' === $entity && is_array( $stored_option ) ? $stored_option : array();
		}

		$this->serve();
	}

	/**
	 * Sets the custom fields for the form.
	 *
	 * @return void
	 */
	public function fields_set(): void {
		$this->verify( array( 'form', 'entity' ) );
		try {
			$entity = isset( $this->data['entity'] ) ? sanitize_key( (string) $this->data['entity'] ) : '';
			if ( empty( $entity ) && isset( $this->request['entity'] ) ) {
				$entity = sanitize_key( $this->request['entity'] );
			}
			if ( empty( $entity ) ) {
				$entity = 'salesorder';
			}
			if ( ! in_array( $entity, array( 'salesorder', 'item' ), true ) ) {
				$entity = 'salesorder';
			}

			$form_data = json_decode( (string) $this->data['form'], true );
			if ( ! is_array( $form_data ) ) {
				$form_data = array();
			}

			$stored_option = get_option( 'cmbird_wootozoho_custom_fields', array() );
			if ( is_string( $stored_option ) ) {
				$decoded = json_decode( $stored_option, true );
				if ( is_array( $decoded ) ) {
					$stored_option = $decoded;
				}
			}

			if ( ! is_array( $stored_option ) || ( ! isset( $stored_option['salesorder'] ) && ! isset( $stored_option['item'] ) ) ) {
				$stored_option = array(
					'salesorder' => array(),
					'item'       => is_array( $stored_option ) ? $stored_option : array(),
				);
			}

			$stored_option[ $entity ] = $form_data;
			update_option( 'cmbird_wootozoho_custom_fields', wp_json_encode( $stored_option ) );
			$this->response = array( 'message' => 'saved' );
		} catch ( Throwable $throwable ) {
			$this->errors = array( 'message' => $throwable->getMessage() );
		}
		$this->serve();
	}

	/**
	 * Resets the fields.
	 *
	 * @return void
	 */
	public function fields_reset(): void {
		$this->verify();

		$entity = isset( $this->data['entity'] ) ? sanitize_key( (string) $this->data['entity'] ) : '';
		if ( empty( $entity ) && isset( $this->request['entity'] ) ) {
			$entity = sanitize_key( $this->request['entity'] );
		}
		if ( in_array( $entity, array( 'salesorder', 'item' ), true ) ) {
			$stored_option = get_option( 'cmbird_wootozoho_custom_fields', array() );
			if ( is_string( $stored_option ) ) {
				$decoded = json_decode( $stored_option, true );
				if ( is_array( $decoded ) ) {
					$stored_option = $decoded;
				}
			}

			if ( is_array( $stored_option ) && ( isset( $stored_option['salesorder'] ) || isset( $stored_option['item'] ) ) ) {
				$stored_option[ $entity ] = array();
				update_option( 'cmbird_wootozoho_custom_fields', wp_json_encode( $stored_option ) );
			} elseif ( 'salesorder' === $entity ) {
					delete_option( 'cmbird_wootozoho_custom_fields' );
			}
		} else {
			delete_option( 'cmbird_wootozoho_custom_fields' );
		}

		$this->response = array( 'message' => 'Reset successfully!' );
		$this->serve();
	}

	/**
	 * Resets the price.
	 *
	 * @return void
	 */
	public function price_reset(): void {
		$this->verify();
		delete_option( 'cmbird_zoho_pricelist_id' );
		delete_option( 'cmbird_zoho_pricelist_role' );
		delete_option( 'cmbird_zoho_pricelist_wcb2b_groups' );
		$this->response = array( 'message' => 'Reset successfully!' );
		$this->serve();
	}

	/**
	 * Retrieves the price information.
	 *
	 * @return void
	 */
	public function price_get(): void {
		$this->verify();
		$this->response['zoho_inventory_pricelist'] = get_option( 'cmbird_zoho_pricelist_id' );
		$this->response['wp_user_role']             = get_option( 'cmbird_zoho_pricelist_role' );
		if ( \CMBIRD_Common_Functions::is_wcb2b_active() ) {
			$this->response['wcb2b'] = get_option( 'cmbird_zoho_pricelist_wcb2b_groups' );
		}
		$this->serve();
	}

	/**
	 * Sets the price and saves the pricelist.
	 */
	public function price_set(): void {
		$this->verify( self::FORMS['price'] );
		try {
			$import_pricelist = new CMBIRD_Pricelist_ZI();
			$success          = $import_pricelist->save_price_list( $this->data );
			if ( class_exists( 'Addify_B2B_Plugin' ) ) {
				update_option( 'cmbird_zoho_pricelist_role', $this->data['wp_user_role'] );
			} else {
				update_option( 'cmbird_zoho_pricelist_wcb2b_groups', $this->data['wcb2b'] );
			}
			if ( $success ) {
				$this->response = array( 'message' => 'Saved' );
				if ( \CMBIRD_Common_Functions::is_wcb2b_active() ) {
					$this->response['wcb2b'] = $this->data['wcb2b'];
				}
			} else {
				$this->errors = array( 'message' => 'Failed to save' );
			}
		} catch ( Throwable $throwable ) {
			$this->errors = array( 'message' => $throwable->getMessage() );
			$this->write_log(
				array(
					'message' => $throwable->getMessage(),
					'files'   => wp_list_pluck( $throwable->getTrace(), 'file', 'line' ),
				),
				'zoho-ajax-error'
			);
		}

		$this->serve();
	}

	/**
	 * Collects Zoho prices.
	 *
	 * @return void
	 */
	public function zoho_prices_collect(): void {
		$this->verify();
		$price_list_class = new CMBIRD_Pricelist_ZI();
		$prices           = $price_list_class->zi_get_all_pricelist();
		$this->response   = wp_list_pluck( $prices, 'name', 'pricebook_id' );
		$this->serve();
	}

	/**
	 * Retrieves the contact details.
	 *
	 * @return void
	 */
	public function contact_get() {
		$this->verify();
		$this->response = $this->option_status_get( self::FORMS['contact'], self::SOURCE );
		$this->serve();
	}
	/**
	 * Sets the contact and updates the status.
	 *
	 * @return void
	 */
	public function contact_set() {
		$this->verify( self::FORMS['contact'] );
		$this->option_status_update( $this->data, self::SOURCE );
		if ( isset( $this->data['enable'] ) && ! empty( $this->data['enable'] ) ) {
			if ( ! wp_next_scheduled( 'zoho_contact_sync' ) ) {
				wp_schedule_event( time(), 'daily', 'zoho_contact_sync' );
			}
		} else {
			wp_clear_scheduled_hook( 'zoho_contact_sync' );
		}
		$this->response = array( 'message' => 'Saved!' );
		$this->serve();
	}
	/**
	 * Resets the contact.
	 *
	 * @return void
	 */
	public function contact_reset() {
		$this->verify();
		$this->option_status_remove( self::FORMS['contact'], self::SOURCE );
		$this->response = array( 'message' => 'Reset successfully!' );
		$this->serve();
	}

	/**
	 * Sets the order and updates the status.
	 *
	 * @return void
	 */
	public function order_set(): void {
		$this->verify( self::FORMS['order'] );
		$this->option_status_update( $this->data, self::SOURCE );
		$this->response = array( 'message' => 'Saved!' );
		$this->serve();
	}

	/**
	 * Retrieves the order.
	 *
	 * @return void
	 */
	public function order_get(): void {
		$this->verify();
		$this->response = $this->option_status_get( self::FORMS['order'], self::SOURCE );
		// option_status_get defaults missing options to ''. Ensure location_country_map.
		// is always returned as an object so the frontend can rely on a consistent type.
		if ( ! is_array( $this->response['location_country_map'] ) ) {
			$this->response['location_country_map'] = new \stdClass();
		}
		$this->serve();
	}

	/**
	 * Resets the order.
	 *
	 * @return void
	 */
	public function order_reset(): void {
		$this->verify();
		$this->option_status_remove( self::FORMS['order'], self::SOURCE );
		$this->response = array( 'message' => 'Reset successfully!' );
		$this->serve();
	}

	/**
	 * Collects Zoho locations.
	 *
	 * This function verifies the request, retrieves the Zoho inventory
	 * organization ID and URL from the options, constructs the URL
	 * for the API call, and executes the cURL call to retrieve the
	 * locations. The response is then processed to extract the
	 * location names and IDs, and stored in the class property.
	 *
	 * @return void
	 */
	public function zoho_locations_collect(): void {
		// $fd = fopen( __DIR__ . '/zoho_locations.txt', 'w+' );
		$this->verify();
		$zoho_inventory_oid       = get_option( 'cmbird_zoho_inventory_oid' );
		$zoho_inventory_url       = get_option( 'cmbird_zoho_inventory_url' );
		$url                      = $zoho_inventory_url . 'inventory/v1/locations?organization_id=' . $zoho_inventory_oid;
		$execute_curl_call_handle = new CMBIRD_API_Handler_Zoho();
		$json                     = $execute_curl_call_handle->execute_curl_call_get( $url );
		// save the first location id to the database of the first object of json.
		if ( ! empty( $json->locations ) && is_array( $json->locations ) && array_key_exists( 0, $json->locations ) ) {
			$location_id = $json->locations[0]->location_id;
			update_option( 'cmbird_zoho_parent_location_id_status', $location_id );
		}
		$this->response = wp_list_pluck( $json->locations, 'location_name', 'location_id' );
		$this->serve();
	}

	/**
	 * Sets the cron for the class.
	 */
	public function cron_set(): void {
		$this->verify( self::FORMS['cron'] );
		if ( array_key_exists( 'form', $this->data ) ) {
			$decode = json_decode( $this->data['form'], true );
			update_option( 'zi_cron_interval', $decode['zi_cron_interval'] );
			unset( $decode['zi_cron_interval'] );
			$this->option_status_update( $decode, self::SOURCE );
		}
		if ( array_key_exists( 'categories', $this->data ) ) {
			$decode = json_decode( $this->data['categories'] );
			update_option( 'cmbird_zoho_item_category', serialize( $decode ) );
		}
		$this->response = array( 'message' => 'Saved' );
		$this->serve();
	}

	/**
	 * Executes the cron_get function.
	 *
	 * This function verifies the form status and retrieves the form options
	 * for syncing name, price, image, and description. It also retrieves
	 * the cron interval and the Zoho item categories.
	 *
	 * @return void
	 */
	public function cron_get(): void {
		$this->verify();
		$this->response                             = array();
		$this->response['form']                     = $this->option_status_get(
			array(
				'disable_name_sync',
				'disable_price_sync',
				'disable_image_sync',
				'disable_description_sync',
			),
			self::SOURCE,
		);
		$this->response['form']['zi_cron_interval'] = get_option( 'zi_cron_interval', 'none' );
		$categories                                 = get_option( 'cmbird_zoho_item_category', '' );
		$selected_zoho_ids                          = maybe_unserialize( $categories );
		if ( ! is_array( $selected_zoho_ids ) ) {
			$selected_zoho_ids = array();
		}

		$map                 = new \CommerceBird\Admin\Mappings\ZohoCategoryMap();
		$resolved_term_ids   = array();
		$unresolved_zoho_ids = array();

		foreach ( $selected_zoho_ids as $zoho_id ) {
			$normalized_zoho_id = (string) $zoho_id;
			if ( '' === $normalized_zoho_id ) {
				continue;
			}

			$term_id = $map->term_id_for_zoho_id_any_status( $normalized_zoho_id );
			if ( null === $term_id ) {
				$unresolved_zoho_ids[] = $normalized_zoho_id;
				continue;
			}

			$resolved_term_ids[] = $term_id;
		}

		$this->response['categories']         = array_values( array_unique( array_map( 'strval', $selected_zoho_ids ) ) );
		$this->response['category_selection'] = array(
			'resolved_term_ids'   => array_values( array_unique( array_map( 'intval', $resolved_term_ids ) ) ),
			'unresolved_zoho_ids' => array_values( array_unique( $unresolved_zoho_ids ) ),
		);
		$this->serve();
	}

	/**
	 * Resets the cron job.
	 *
	 * This function verifies the cron job, removes the specified options,
	 * deletes the 'zoho_item_category' option, and serves the cron job.
	 *
	 * @return void
	 */
	public function cron_reset(): void {
		$this->verify();
		$this->option_status_remove(
			array(
				'disable_name_sync',
				'disable_price_sync',
				'disable_image_sync',
				'disable_description_sync',
			),
			self::SOURCE,
		);
		delete_option( 'cmbird_zoho_item_category' );
		// Delete category mappings from the new table.
		$map          = new \CommerceBird\Admin\Mappings\ZohoCategoryMap();
		$count_before = count( $map->list( 0, PHP_INT_MAX, 'active' ) );
		$count_review = count( $map->list( 0, PHP_INT_MAX, 'review' ) );
		$count_orphan = count( $map->list( 0, PHP_INT_MAX, 'orphan' ) );

		global $wpdb;
		$deleted_rows = (int) $wpdb->query(
			"DELETE FROM {$wpdb->prefix}cmbird_zi_category_map"
		);

		$total          = $count_before + $count_review + $count_orphan;
		$this->response = array( 'message' => "Reset successfully! Found {$total} category mapping rows, deleted {$deleted_rows}." );
		$this->serve();
	}

	/**
	 * Collects all categories from both Zoho Inventory and WooCommerce.
	 *
	 * This function retrieves categories from Zoho Inventory and merges them with
	 * WooCommerce categories to provide a complete list for the frontend display.
	 * This ensures all 66+ categories are visible, not just the ones in Zoho.
	 */
	public function zoho_categories_collect(): void {
		$this->verify();
		$category_class = new CMBIRD_Categories_ZI();

		// Only return Zoho categories since we're storing Zoho category IDs in options.
		$zoho_categories = $category_class->cmbird_get_zoho_item_categories();
		if ( gettype( $zoho_categories ) === 'array' ) {
			$this->response = $zoho_categories;
		} else {
			$this->response = array();
		}
		$this->serve();
	}

	/**
	 * Returns paginated active category mappings, enriched with WC term + Zoho category names.
	 */
	public function category_mappings_get(): void {
		$this->verify();

		$map  = new \CommerceBird\Admin\Mappings\ZohoCategoryMap();
		$rows = $map->list( 0, 500, 'all' );

		$zoho_categories = ( new \CMBIRD_Categories_ZI() )->cmbird_get_zoho_item_categories();
		$zoho_lookup     = array();
		if ( is_array( $zoho_categories ) ) {
			foreach ( $zoho_categories as $z ) {
				$zoho_lookup[ (string) $z['category_id'] ] = (string) ( $z['category_name'] ?? '' );
			}
		}

		$enriched = array();
		foreach ( $rows as $row ) {
			$term       = get_term( (int) $row['wc_term_id'], 'product_cat' );
			$term_name  = ( $term && ! is_wp_error( $term ) ) ? $term->name : sprintf( '(deleted term #%d)', $row['wc_term_id'] );
			$enriched[] = array(
				'row_id'             => (int) $row['id'],
				'wc_term_id'         => (int) $row['wc_term_id'],
				'wc_term_name'       => $term_name,
				'zoho_category_id'   => (string) $row['zoho_category_id'],
				'zoho_category_name' => $zoho_lookup[ (string) $row['zoho_category_id'] ] ?? '(unknown)',
				'source'             => (string) $row['source'],
				'status'             => (string) $row['status'],
				'review_reason'      => (string) ( $row['review_reason'] ?? '' ),
				'last_verified_at'   => $row['last_verified_at'],
			);
		}

		$this->response = $enriched;
		$this->serve();
	}

	/**
	 * Returns review and orphan rows merged for operator triage.
	 */
	public function category_review_queue_get(): void {
		$this->verify();

		$map  = new \CommerceBird\Admin\Mappings\ZohoCategoryMap();
		$rows = array_merge(
			$map->list( 0, 200, 'review' ),
			$map->list( 0, 200, 'orphan' )
		);

		$zoho_categories = ( new \CMBIRD_Categories_ZI() )->cmbird_get_zoho_item_categories();
		$zoho_lookup     = array();
		if ( is_array( $zoho_categories ) ) {
			foreach ( $zoho_categories as $z ) {
				$zoho_lookup[ (string) $z['category_id'] ] = (string) ( $z['category_name'] ?? '' );
			}
		}

		$enriched = array();
		foreach ( $rows as $row ) {
			$term      = get_term( (int) $row['wc_term_id'], 'product_cat' );
			$term_name = ( $term && ! is_wp_error( $term ) ) ? $term->name : sprintf( '(deleted term #%d)', $row['wc_term_id'] );

			// Find the active conflict, if any.
			global $wpdb;
			$conflict = $wpdb->get_row(
				$wpdb->prepare(
					"SELECT wc_term_id, zoho_category_id FROM {$wpdb->prefix}cmbird_zi_category_map
					 WHERE status = 'active'
					   AND ( wc_term_id = %d OR zoho_category_id = %s )
					 LIMIT 1",
					(int) $row['wc_term_id'],
					(string) $row['zoho_category_id']
				),
				ARRAY_A
			);

			$enriched[] = array(
				'row_id'             => (int) $row['id'],
				'wc_term_id'         => (int) $row['wc_term_id'],
				'wc_term_name'       => $term_name,
				'zoho_category_id'   => (string) $row['zoho_category_id'],
				'zoho_category_name' => $zoho_lookup[ (string) $row['zoho_category_id'] ] ?? '(unknown)',
				'source'             => (string) $row['source'],
				'status'             => (string) $row['status'],
				'last_verified_at'   => $row['last_verified_at'],
				'review_reason'      => (string) $row['review_reason'],
				'conflicts_with'     => $conflict ?: null,
			);
		}

		$this->response = $enriched;
		$this->serve();
	}

	/**
	 * Delete a single category mapping row by its ID.
	 */
	public function category_mapping_delete(): void {
		$this->verify( array( 'row_id' ) );

		$row_id  = (int) $this->data['row_id'];
		$deleted = ( new \CommerceBird\Admin\Mappings\ZohoCategoryMap() )->delete_by_id( $row_id );

		if ( $deleted ) {
			$this->response = array( 'message' => 'Mapping removed' );
		} else {
			$this->errors = array( 'message' => 'Mapping not found or already removed' );
		}
		$this->serve();
	}

	/**
	 * Create or update a manual category mapping.
	 */
	public function category_mapping_set_manual(): void {
		$this->verify( array( 'wc_term_id', 'zoho_category_id' ) );

		$map    = new \CommerceBird\Admin\Mappings\ZohoCategoryMap();
		$result = $map->set_manual(
			(int) $this->data['wc_term_id'],
			(string) $this->data['zoho_category_id']
		);

		$this->response = array(
			'message' => 'Manual mapping saved',
			'status'  => $result->status,
			'row_id'  => $result->row_id,
		);
		$this->serve();
	}

	/**
	 * Resolve a category mapping review.
	 */
	public function category_review_resolve(): void {
		$this->verify( array( 'row_id', 'resolution' ) );

		$resolution = match ( (string) $this->data['resolution'] ) {
			'keep_existing'        => \CommerceBird\Admin\Mappings\ReviewResolution::KeepExisting,
			'replace_with_pending' => \CommerceBird\Admin\Mappings\ReviewResolution::ReplaceWithPending,
			'discard_both'         => \CommerceBird\Admin\Mappings\ReviewResolution::DiscardBoth,
			default                => null,
		};

		if ( null === $resolution ) {
			$this->errors = array( 'message' => 'Invalid resolution' );
			$this->serve();
			return;
		}

		( new \CommerceBird\Admin\Mappings\ZohoCategoryMap() )->resolve_review(
			(int) $this->data['row_id'],
			$resolution
		);

		$this->response = array( 'message' => 'Resolved' );
		$this->serve();
	}

	/**
	 * Executes the connection process and handles the response.
	 */
	public function connection_done(): void {
		$this->verify();
		$zoho_inventory_url = get_option( 'cmbird_zoho_inventory_url' );
		$zoho_inventory_oid = get_option( 'cmbird_zoho_inventory_oid' );
		$url                = $zoho_inventory_url . 'inventory/v1/apiusagereport/dashboard?version=all&organization_id=' . $zoho_inventory_oid;

		$execute_curl_call_handle = new CMBIRD_API_Handler_Zoho();
		$json                     = $execute_curl_call_handle->execute_curl_call_get( $url );

		if ( is_wp_error( $json ) ) {
			$this->errors = array( 'message' => $json->get_error_message() );
			return;
		}

		if ( empty( $json ) || empty( $json->data ) || empty( $json->data->api_usage ) ) {
			$this->errors = array( 'message' => 'We lost connection with Zoho. Please refresh the page.' );
			return;
		}
		$api_usage = $json->data->api_usage;
		// Debug JSON encoding.
		$encoded_api_usage = wp_json_encode( $api_usage );
		// Ensure it's not null.
		$this->response = json_decode( $encoded_api_usage, true ) ?? array();
		$this->serve();
	}

	/**
	 * Executes the product_set function.
	 */
	public function product_set(): void {
		$this->verify( self::FORMS['product'] );
		if ( ! empty( $this->data ) ) {
			$this->option_status_update( $this->data, self::SOURCE );
		}
		$this->response = array( 'message' => 'Saved!' );
		$this->serve();
	}

	/**
	 * Retrieves the product data.
	 *
	 * @return void
	 */
	public function product_get(): void {
		$this->verify();
		$this->response = $this->option_status_get( self::FORMS['product'], self::SOURCE );
		$this->serve();
	}

	/**
	 * Resets the product.
	 *
	 * @return void
	 */
	public function product_reset(): void {
		$this->verify();
		$this->option_status_remove( self::FORMS['product'], self::SOURCE );
		$this->response = array( 'message' => 'Reset successfully!' );
		$this->serve();
	}

	/**
	 * Retrieves the tax information.
	 *
	 * @return void
	 */
	public function tax_get(): void {
		$this->verify();
		$this->response = array();
		foreach ( $this->wc_taxes() as $tax ) {
			$this->response['selectedTaxRates'][] = $tax['id'] . '^^' . get_option( 'cmbird_zoho_inventory_tax_rate_' . $tax['id'] );
		}
		$this->serve();
	}

	/**
	 * Retrieves an array of all tax rates from WooCommerce.
	 *
	 * @return array An array of all tax rates.
	 */
	public function wc_taxes(): array {
		$wc_tax_array = array();
		$tax_classes  = WC_Tax::get_tax_classes(); // Retrieve all tax classes.
		if ( ! in_array( '', $tax_classes ) ) { // Make sure "Standard rate" (empty class name) is present.
			array_unshift( $tax_classes, '' );
		}

		foreach ( $tax_classes as $tax_class ) { // For each tax class, get all rates.
			$taxes = WC_Tax::get_rates_for_tax_class( $tax_class );

			foreach ( $taxes as $key => $tax ) {
				$taxarray       = (array) $tax;
				$taxarray['id'] = $key;
				$wc_tax_array[] = $taxarray;
			}
		}

		return $wc_tax_array;
	}

	/**
	 * Resets the tax settings.
	 *
	 * This function verifies the tax settings and then deletes the options
	 * related to the tax rates and tax groups.
	 *
	 * @return void
	 */
	public function tax_reset(): void {
		$this->verify();
		foreach ( $this->wc_taxes() as $tax ) {
			delete_option( 'cmbird_zoho_inventory_tax_rate_' . $tax['id'] );
		}
		delete_option( 'cmbird_zi_tax_groups' );
		$this->response = array( 'message' => 'Reset successfully!' );
		$this->serve();
	}

	/**
	 * Saves tax rates to the database.
	 */
	public function tax_set(): void {
		$this->verify( self::FORMS['tax'] );
		$rates = array_filter( $this->data['selectedTaxRates'] );
		try {
			foreach ( $rates as $value ) {
				$valarray = explode( '^^', $value );
				update_option( 'cmbird_zoho_inventory_tax_rate_' . $valarray[0], $valarray[1] );
			}
			$this->response = array( 'message' => 'Saved' );
		} catch ( Throwable $throwable ) {
			$this->errors = array( 'message' => $throwable->getMessage() );
		}

		$this->serve();
	}

	/**
	 * Sets the connection for the Zoho Inventory API.
	 */
	public function connection_set(): void {
		$this->verify(
			array(
				'account_domain',
				'organization_id',
				'client_id',
				'client_secret',
				'redirect_uri',
			),
		);
		try {
			$domain     = CMBIRD_Auth_Zoho::normalize_domain( $this->data['account_domain'] );
			$inventory  = sprintf( 'https://www.zohoapis.%s/', $domain );
			$oauth_args = array(
				'response_type' => 'code',
				'client_id'     => $this->data['client_id'],
				'scope'         => 'ZohoInventory.FullAccess.all',
				'redirect_uri'  => $this->data['redirect_uri'],
				'prompt'        => 'consent',
				'access_type'   => 'offline',
				'state'         => wp_create_nonce( Template::NAME ),
			);

			update_option( 'cmbird_zoho_inventory_domain', $domain );
			update_option( 'cmbird_zoho_inventory_oid', $this->data['organization_id'] );
			update_option( 'cmbird_zoho_inventory_cid', $this->data['client_id'] );
			update_option( 'cmbird_zoho_inventory_cs', $this->data['client_secret'] );
			update_option( 'cmbird_zoho_inventory_url', $inventory );
			update_option( 'cmbird_authorization_redirect_uri', $this->data['redirect_uri'] );
			$redirect_base  = CMBIRD_Auth_Zoho::get_auth_redirect_url_for_domain( $domain );
			$redirect       = esc_url_raw( add_query_arg( $oauth_args, $redirect_base ) );
			$this->response = array(
				'redirect' => $redirect,
				'message'  => 'We are redirecting you to zoho. please wait...',
			);
		} catch ( Throwable $throwable ) {
			$this->errors = array( 'message' => $throwable->getMessage() );
		}

		$this->serve();
	}

	/**
	 * Resets the connection by deleting specific options from the database.
	 */
	public function connection_reset(): void {
		$this->verify();
		try {
			// Check if resetAll is requested (sent from Vue).
			$reset_all = isset( $this->data['reset'] ) && ( $this->data['reset'] === 1 || $this->data['reset'] === true || $this->data['reset'] === 'true' );

			// Safety: require an explicit confirmation token for destructive, global reset.
			$confirm_token = isset( $this->data['confirm'] ) ? $this->data['confirm'] : '';

			if ( $reset_all ) {
				// The client must pass confirm: 'reset-all' to perform the full destructive reset.
				if ( 'reset-all' !== $confirm_token ) {
					$this->errors = array( 'message' => 'Missing reset confirmation.' );
					$this->serve();
					return;
				}

				// Audit log for full reset (important to have server-side evidence).
				$this->write_log(
					array(
						'action'  => 'full_reset_triggered',
						'user_id' => get_current_user_id(),
					),
					'zoho-reset'
				);

				global $wpdb;
				// Delete all product and customer meta keys.
				$wpdb->query( "DELETE FROM {$wpdb->prefix}postmeta WHERE meta_key IN ('zi_item_id', 'zi_category_id')" );
				$wpdb->query( "DELETE FROM {$wpdb->prefix}usermeta WHERE meta_key IN ('zi_contact_id', 'zi_primary_contact_id', 'zi_billing_address_id', 'zi_shipping_address_id', 'zi_contact_persons_id')" );

				// Delete all plugin options that belong to Zoho namespace.
				$wpdb->query(
					$wpdb->prepare(
						"DELETE FROM {$wpdb->prefix}options WHERE option_name LIKE %s",
						'cmbird_zoho_%'
					)
				);

				$this->response['message'] = __( 'All settings are reset successfully', 'commercebird' );
				$this->serve();
				return;
			}

			// Non-destructive reset (connection settings only).
			$options = array(
				'cmbird_zoho_inventory_domain',
				'cmbird_zoho_inventory_oid',
				'cmbird_zoho_inventory_cid',
				'cmbird_zoho_inventory_cs',
				'cmbird_zoho_inventory_url',
				'cmbird_zoho_inventory_access_token',
			);
			foreach ( $options as $zi_option ) {
				delete_option( $zi_option );
			}
			$this->response = array( 'message' => 'Reset successfully!' );

		} catch ( Throwable $throwable ) {
			$this->errors = array( 'message' => $throwable->getMessage() );
		}

		$this->serve();
	}

	/**
	 * Retrieves the Zoho tax rates for the organization.
	 *
	 * This function verifies the request, retrieves the organization ID and URL
	 * from the options, constructs the URL for the API request, and executes
	 * the cURL call to fetch the tax rates. If the response contains the
	 * 'taxes' key, it sets the response to the tax rates and saves tax groups
	 * separately. Otherwise, it sets the errors to a default message.
	 */
	public function zoho_tax_rates_collect(): void {
		$this->verify();
		$zoho_inventory_oid = get_option( 'cmbird_zoho_inventory_oid' );
		$zoho_inventory_url = get_option( 'cmbird_zoho_inventory_url' );
		$url                = $zoho_inventory_url . 'inventory/v1/settings/taxes?organization_id=' . $zoho_inventory_oid;
		try {
			$execute_curl_call_handle = new CMBIRD_API_Handler_Zoho();
			$json                     = (array) $execute_curl_call_handle->execute_curl_call_get( $url );
			if ( array_key_exists( 'taxes', $json ) ) {
				$this->response = $json['taxes'];

				// Save tax groups separately for compound tax handling.
				$tax_groups = array();
				foreach ( $json['taxes'] as $tax ) {
					if ( isset( $tax->tax_type ) && 'tax_group' === $tax->tax_type ) {
						$tax_groups[] = array(
							'tax_id'         => $tax->tax_id,
							'tax_percentage' => $tax->tax_percentage,
							'tax_name'       => $tax->tax_name,
						);
					}
				}
				update_option( 'cmbird_zi_tax_groups', $tax_groups, false );
			} else {
				$this->errors = array( 'message' => 'Something went wrong!' );
			}
		} catch ( Throwable $throwable ) {
			$this->errors = array( 'message' => $throwable->getMessage() );
		}

		$this->serve();
	}

	/**
	 * Executes the wc_tax_collect function.
	 *
	 * This function verifies the current state, retrieves the taxes using the
	 * wc_taxes method, and serves the response.
	 */
	public function wc_tax_collect(): void {
		$this->verify();
		$this->response = $this->wc_taxes();
		$this->serve();
	}

	/**
	 * Handles the oAuth code.
	 */
	public function handle_code(): void {
		$this->verify();
		if ( array_key_exists( 'code', $this->request ) ) {
			$class_functions = new CMBIRD_Auth_Zoho();
			$code            = $this->request['code'];
			$state           = $this->request['state'] ?? '';

			if ( empty( $state ) || ! wp_verify_nonce( $state, Template::NAME ) ) {
				$this->errors = array( 'message' => 'Invalid OAuth state. Please try connecting again.' );
				$this->serve();
				return;
			}

			try {
				$access_token = $class_functions->get_zoho_access_token( $code, 'zoho_inventory' );
				if ( is_wp_error( $access_token ) ) {
					$this->errors = array( 'message' => $access_token->get_error_message() );
				} elseif ( is_array( $access_token ) && array_key_exists( 'access_token', $access_token ) ) {
					update_option( 'cmbird_zoho_inventory_auth_code', $code );
					update_option( 'cmbird_zoho_inventory_access_token', $access_token['access_token'] );
					update_option( 'cmbird_zoho_inventory_refresh_token', $access_token['refresh_token'] );
					update_option( 'cmbird_zoho_inventory_timestamp', strtotime( gmdate( 'Y-m-d H:i:s' ) ) + $access_token['expires_in'] );
					$this->response = (array) $access_token;
				} else {
					$this->errors = array( 'message' => 'Unable to retrieve Zoho Inventory access token.' );
				}
			} catch ( Throwable $throwable ) {
				$this->errors = array( 'message' => $throwable->getMessage() );
			}
		}
		$this->serve();
	}

	/**
	 * Converts all guest users to a registered users.
	 *
	 * This function verifies the request, retrieves all guest orders
	 *
	 * @return void
	 */
	public function convert_guest(): void {
		$this->verify();
		$args = array(
			'status'   => array( 'wc-processing', 'wc-completed' ),
			'return'   => 'ids',
			'customer' => 0, // Only guest orders.
			'limit'    => -1, // Retrieve all guest orders.
		);

		$orders = wc_get_orders( $args );
		if ( empty( $orders ) ) {
			$this->response = array( 'message' => __( 'No guest orders found.', 'commercebird' ) );
			$this->serve();
			return;
		}
		$count = count( $orders );
		foreach ( $orders as $order_id ) {
			$order = wc_get_order( $order_id );
			$email = $order->get_billing_email();

			if ( email_exists( $email ) === false ) {
				$username = strstr( $email, '@', true );
				$password = wp_generate_password();
				$user_id  = wc_create_new_customer( $email, $username, $password );

				if ( ! is_wp_error( $user_id ) ) {
					// use update_meta to set the user ID for the order.
					$order->set_customer_id( $user_id );
					$order->save();
				}
			} else {
				// If the email already exists, we can still update the order to the existing user.
				$user = get_user_by( 'email', $email );
				if ( $user ) {
					$order->set_customer_id( $user->ID );
					$order->save();
				}
			}
		}
		/* translators: %d: Number of orders converted */
		$this->response['message'] = sprintf( __( 'Successfully converted %d guest orders to registered users.', 'commercebird' ), $count );
		$this->serve();
	}   /**
		 * Fetches webhooks from Zoho Inventory API.
		 *
		 * @return void
		 */
	public function get_zoho_webhooks(): void {
		$this->verify();
		$zoho_inventory_oid = get_option( 'cmbird_zoho_inventory_oid' );
		$zoho_inventory_url = get_option( 'cmbird_zoho_inventory_url' );
		if ( ! $zoho_inventory_oid || ! $zoho_inventory_url ) {
			$this->errors = array( 'message' => 'Missing Zoho organization ID or domain.' );
			$this->serve();
			return;
		}
		$url = $zoho_inventory_url . 'inventory/v1/settings/webhooks?page=1&per_page=50&usestate=false&organization_id=' . $zoho_inventory_oid;
		try {
			$execute_curl_call_handle = new CMBIRD_API_Handler_Zoho();
			$json                     = $execute_curl_call_handle->execute_curl_call_get( $url );

			// Check if the response is a WP_Error object.
			if ( is_wp_error( $json ) ) {
				$this->errors = array( 'message' => $json->get_error_message() );
			} elseif ( isset( $json->webhooks ) && is_array( $json->webhooks ) ) {
				$this->response = $json->webhooks;
			} else {
				$this->errors = array( 'message' => 'Failed to fetch webhooks or no webhooks found.' );
			}
		} catch ( Throwable $throwable ) {
			$this->errors = array( 'message' => $throwable->getMessage() );
		}
		$this->serve();
	}

	/**
	 * Function to sync variable items from Zoho to WooCommerce.
	 */
	public function sync_variable_items_from_zoho(): void {
		$this->verify();

		// Clear Orphan data.
		$zi_common_class = new \CMBIRD_Common_Functions();
		$zi_common_class->clear_orphan_data();

		// check if a category is selected.
		$selected_category = isset( $_GET['category'] ) ? sanitize_text_field( $_GET['category'] ) : null;

		if ( $selected_category ) {
			$categories = array( $selected_category ); // Only sync the selected category.
		} else {
			$zoho_item_category = get_option( 'cmbird_zoho_item_category' );
			if ( $zoho_item_category ) {
				// convert serialized string to array.
				$categories = maybe_unserialize( $zoho_item_category );
				if ( ! is_array( $categories ) ) {
					$categories = array();
				}
			} else {
				$categories = array();
			}
		}

		if ( empty( $categories ) ) {
			$this->errors = array( 'message' => __( 'Please select at least one category from cron tab', 'commercebird' ) );
		} else {
			foreach ( $categories as $category_index => $category_id ) {
				$data              = array(
					'page'     => 1,
					'category' => $category_id,
				);
				$existing_schedule = as_has_scheduled_action( 'import_group_items_cron', $data );
				// Schedule the action if it doesn't exist.
				if ( ! $existing_schedule ) {
					as_schedule_single_action( time(), 'import_group_items_cron', $data );
				}
			}
			$this->response = array( 'message' => 'Items are being imported in background. You can visit other tabs :).' );
		}
		$this->serve();
	}

	/**
	 * Function to sync simple items from Zoho to WooCommerce.
	 */
	public function sync_simple_items_from_zoho(): void {
		$this->verify();

		// Clear Orphan data.
		$zi_common_class = new \CMBIRD_Common_Functions();
		$zi_common_class->clear_orphan_data();

		// Check if a category is selected.
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Admin sync action, nonce verified in parent.
		$selected_category = isset( $_GET['category'] ) ? sanitize_text_field( wp_unslash( $_GET['category'] ) ) : null;

		if ( $selected_category ) {
			$categories = array( $selected_category ); // Only sync the selected category.
		} else {
			$zoho_item_category = get_option( 'cmbird_zoho_item_category' );
			if ( $zoho_item_category ) {
				// convert serialized string to array.
				$categories = maybe_unserialize( $zoho_item_category );
				if ( ! is_array( $categories ) ) {
					$categories = array();
				}
			} else {
				$categories = array();
			}
		}

		if ( empty( $categories ) ) {
			$this->errors = array( 'message' => __( 'Please select at least one category from cron tab', 'commercebird' ) );
		} else {
			foreach ( $categories as $index => $category_id ) {
				$data = array(
					'page'     => 1,
					'category' => $category_id,
				);
				// Add group parameter to prevent scheduling conflicts.
				$existing_schedule = as_has_scheduled_action( 'import_simple_items_cron', $data, 'commercebird' );
				if ( ! $existing_schedule ) {
					as_schedule_single_action( time(), 'import_simple_items_cron', $data, 'commercebird' );
				}
			}
			$this->response = array( 'message' => __( 'Items are being imported in background. You can visit other tabs :).', 'commercebird' ) );
		}
		$this->serve();
	}


	/**
	 * Schedule a batch of WooCommerce product IDs for Zoho sync.
	 *
	 * Wrapped in a protected method to make testing easier.
	 *
	 * @param int[] $ids Array of post IDs to sync.
	 * @return void
	 */
	protected function schedule_zi_product( array $ids ): void {
		as_schedule_single_action( time(), 'sync_zi_product_cron', array( $ids ) );
	}

	/**
	 * Resolve the stored Zoho category IDs into WooCommerce product_cat term IDs.
	 *
	 * The option 'cmbird_zoho_item_category' is a plain serialised array of Zoho
	 * category IDs. For each ID the corresponding WC term ID is resolved by
	 * querying the per-term options stored as 'cmbird_zoho_id_for_term_id_{term_id}'.
	 *
	 * @param mixed $opt The raw option value returned by get_option().
	 * @return int[] List of WC product_cat term IDs (empty array on failure).
	 */
	private function normalize_category_list( $opt ): array {
		if ( empty( $opt ) ) {
			return array();
		}

		$zoho_cat_ids = maybe_unserialize( $opt );
		if ( ! is_array( $zoho_cat_ids ) ) {
			return array();
		}

		$map      = new \CommerceBird\Admin\Mappings\ZohoCategoryMap();
		$term_ids = array();
		foreach ( $zoho_cat_ids as $zoho_id ) {
			if ( empty( $zoho_id ) ) {
				continue;
			}
			$term_id = $map->term_id_for_zoho_id_any_status( (string) $zoho_id );
			if ( null !== $term_id ) {
				$term_ids[] = $term_id;
			}
		}

		return array_values( array_unique( $term_ids ) );
	}

	/**
	 * Return all published products in the given categories which do not have a
	 * Zoho item ID yet.
	 *
	 * @param int[] $categories Array of product_cat term IDs to filter by.
	 * @return int[] List of post IDs.
	 */
	protected function get_unsynced_product_ids( array $categories ): array {
		// make sure we actually have some categories to query against.
		if ( empty( $categories ) ) {
			return array();
		}

		// cast everything to int just in case the caller passed strings.
		$categories = array_map( 'intval', $categories );

		$args = array(
			'post_type'      => 'product',
			'post_status'    => 'publish',
			'fields'         => 'ids',
			'posts_per_page' => -1,
			'tax_query'      => array(
				array(
					'taxonomy' => 'product_cat',
					'field'    => 'term_id',
					'terms'    => $categories,
				),
			),
			// use a meta_query so the database only returns posts without the meta key.
			'meta_query'     => array(
				array(
					'key'     => 'zi_item_id',
					'compare' => 'NOT EXISTS',
				),
			),
		);

		// WP_Query handles the heavy lifting; since we asked for IDs only we get a simple array.
		$query = new \WP_Query( $args );

		return $query->posts ?: array();
	}

	/**
	 * Function to sync items from WooCommerce to Zoho.
	 */
	public function sync_items_to_zoho(): void {
		$this->verify();

		// figure out which categories should be considered for this sync; it can.
		// be passed explicitly via GET or fall back to the stored cron setting.
		$selected_category = isset( $_GET['category'] ) ? sanitize_text_field( wp_unslash( $_GET['category'] ) ) : null;

		if ( $selected_category ) {
			$categories = array( $selected_category );
		} else {
			$categories = $this->normalize_category_list( get_option( 'cmbird_zoho_item_category' ) );
		}

		if ( empty( $categories ) ) {
			$this->errors = array( 'message' => __( 'Please select at least one category from category page', 'commercebird' ) );
			$this->serve();
			return;
		}

		// get all unsynced products within those categories.
		$post_ids = $this->get_unsynced_product_ids( $categories );

		if ( empty( $post_ids ) ) {
			$this->response = array( 'message' => 'No items to sync.' );
			$this->serve();
			return;
		}       // get all unsynced products within those categories.
		$post_ids = $this->get_unsynced_product_ids( $categories );

		if ( empty( $post_ids ) ) {
			$this->response = array( 'message' => 'No items to sync.' );
			$this->serve();
			return;
		}

		$product_ids = array();
		foreach ( $post_ids as $post_id ) {
			$product_ids[] = $post_id;
			if ( count( $product_ids ) === 10 ) {
				$this->schedule_zi_product( $product_ids );
				$product_ids = array();
			}
		}

		if ( count( $product_ids ) > 0 ) {
			$this->schedule_zi_product( $product_ids );
		}

		$this->response = array( 'message' => 'Products are being synced in background.' );
		$this->serve();
	}

	/**
	 * Sync contacts from Zoho to WooCommerce.
	 */
	public function sync_contacts_from_zoho(): void {
		$this->verify();

		$page              = 1;
		$data_arr          = (object) array();
		$data_arr->page    = $page;
		$existing_schedule = as_has_scheduled_action( 'sync_zi_import_contacts', array( $data_arr ) );

		// Wrap this via Action Scheduler per page.
		if ( ! $existing_schedule ) {
			// Schedule the cron job.
			as_schedule_single_action( time(), 'sync_zi_import_contacts', array( $data_arr ) );
		}

		$this->response = array( 'message' => 'Syncing in background. You can visit other tabs :).' );
		$this->serve();
	}

	/**
	 * Sync composite items from Zoho to WooCommerce.
	 */
	public function sync_composite_items_from_zoho(): void {
		$this->verify();

		// Clear Orphan data.
		$zi_common_class = new \CMBIRD_Common_Functions();
		$zi_common_class->clear_orphan_data();

		// check if a category is selected.
		$selected_category = isset( $_GET['category'] ) ? sanitize_text_field( $_GET['category'] ) : null;
		if ( $selected_category ) {
			$categories = array( $selected_category ); // Only sync the selected category.
		} else {
			$zoho_item_category = get_option( 'cmbird_zoho_item_category' );
			if ( $zoho_item_category ) {
				// convert serialized string to array.
				$categories = maybe_unserialize( $zoho_item_category );
				if ( ! is_array( $categories ) ) {
					$categories = array();
				}
			} else {
				$categories = array();
			}
		}

		$item_add_resp = array();
		foreach ( $categories as $category_id ) {
			$product_class = new \CMBIRD_Products_ZI();
			$response      = $product_class->recursively_sync_composite_item_from_zoho( 1, $category_id );
			$item_add_resp = array_merge( $item_add_resp, $response );
		}

		// Send log message to admin if there are responses.
		if ( ! empty( $item_add_resp ) ) {
			$this->send_log_message_to_admin( $item_add_resp, 'Log Message for manual sync', 'Composite item sync from zoho' );
		}

		$this->response = $item_add_resp;
		$this->serve();
	}

	/**
	 * Sync composite items from WooCommerce to Zoho.
	 */
	public function sync_composite_items_to_zoho(): void {
		$this->verify();

		$irgs = array(
			'post_type'      => array( 'product' ),
			'posts_per_page' => '-1',
			'post_status'    => 'publish',
			'tax_query'      => array(
				array(
					'taxonomy' => 'product_type',
					'field'    => 'slug',
					'terms'    => 'bundle',
				),
			),
		);

		$my_query        = new \WP_Query( $irgs );
		$bundled_product = $my_query->posts;
		if ( count( $bundled_product ) > 0 ) {
			foreach ( $bundled_product as $prod ) {
				$bundle_childs = \WC_PB_DB::query_bundled_items(
					array(
						'return'    => 'id=>product_id', // 'objects'.
						'bundle_id' => array( $prod->ID ),
					)
				);
				if ( count( $bundle_childs ) > 0 ) {
					foreach ( $bundle_childs as $child_item_id ) {
						$zoho_item_id = get_post_meta( $child_item_id, 'zi_item_id', true );
						if ( empty( $zoho_item_id ) ) {
							break;
						}
					}
				}
			}
		}

		$this->response = array(
			'message' => 'Sync Started',
			'code'    => 200,
		);
		$this->serve();
	}

	/**
	 * Send log message to admin.
	 *
	 * @param array  $sync_logs Log messages.
	 * @param string $subject Email subject.
	 * @param string $message Email message.
	 */
	private function send_log_message_to_admin( $sync_logs, $subject, $message ): void {
		$table_root  = "<h3>$message</h3>";
		$table_root .= '<table><thead><tr><th>Action</th><th> Log message</th></tr></thead><tbody>';

		foreach ( $sync_logs as $logs ) {
			$table_root .= "<tr><td>$logs->resp_id</td><td>$logs->message</td></tr>";
		}
		$table_root  .= '</tbody></table>';
		$common_class = new \CMBIRD_Common_Functions();
		$common_class->send_email( $subject, $table_root );
	}
}
