<?php
/**
 * The ActiveCampaign class
 *
 * Used to define ActiveCampaign related functions
 *
 *
 * @since      5.0
 * @package    CartBounty Pro - Save and recover abandoned carts for WooCommerce
 * @subpackage CartBounty Pro - Save and recover abandoned carts for WooCommerce/includes
 * @author     Streamline.lv
 */
class CartBounty_Pro_ActiveCampaign{

	/**
	 * The admin handler that manages the plugin's settings, options, and backend functionality.
	 *
	 * @since    10.9
	 * @access   protected
	 * @var      CartBounty_Pro_Admin    $admin    Provides methods to control and extend the plugin's admin area.
	 */
	protected $admin = null;

	/**
	 * The coupons manager responsible for handling coupons.
	 *
	 * @since    10.9
	 * @access   protected
	 * @var      CartBounty_Pro_Coupons    $coupons    Provides methods to create, apply, and manage discount coupons.
	 */
	protected $coupons = null;

	/**
	 * The API connector responsible for handling communication with external services.
	 *
	 * @since    10.9
	 * @access   protected
	 * @var      CartBounty_Pro_API_Connector    $api    Provides methods to send and receive data via API requests.
	 */
	protected $api = null;

	/**
	 * The API access credentials used to authenticate requests with the external service.
	 *
	 * @since    10.9
	 * @access   protected
	 * @var      array    $api_access    Contains authentication data such as API key, token, or other required parameters.
	 */
	protected $api_access = null;

	/**
	 * Get the admin handler (lazy-loaded).
	 * Creates the connector on first use and then reuses the same instance.
	 *
	 * @since 10.9
	 * @access protected
	 * @return CartBounty_Pro_Admin
	 */
	protected function admin(){
		
		if( $this->admin === null ){
			$this->admin = new CartBounty_Pro_Admin( CARTBOUNTY_PRO_PLUGIN_NAME_SLUG, CARTBOUNTY_PRO_VERSION_NUMBER );
		}

		return $this->admin;
	}

	/**
	 * Get the coupon handler (lazy-loaded).
	 * Creates the connector on first use and then reuses the same instance.
	 *
	 * @since 10.9
	 * @access protected
	 * @return CartBounty_Pro_Coupons
	 */
	protected function coupons(){
		
		if( $this->coupons === null ){
			$this->coupons = new CartBounty_Pro_Coupons();
		}

		return $this->coupons;
	}

	/**
	 * Get the API connector (lazy-loaded).
	 * Creates the connector on first use and then reuses the same instance.
	 *
	 * @since 10.9
	 * @access protected
	 * @return CartBounty_Pro_API_Connector
	 */
	protected function api(){
		
		if( $this->api === null ){
			$this->api = new CartBounty_Pro_API_Connector();
		}

		return $this->api;
	}

	/**
	 * Get the API access creentials (lazy-loaded).
	 * Creates the connector on first use and then reuses the same instance.
	 *
	 * @since 10.9
	 * @access protected
	 * @return array
	 */
	protected function api_access(){
		
		if( $this->api_access === null ){
			$this->api_access = $this->get_api_access();
		}

		return $this->api_access;
	}
	
	/**
	* Retrieve ActiveCampaign recovery settings
	*
	* @since    10.1
	* @return   array
	* @param    string     $value                Value to return
	*/
	public function get_settings( $value = false ){
		$saved_options = get_option( 'cartbounty_pro_activecampaign_settings' );
		$defaults = array(
			'key' 					=> '',
			'url' 					=> '',
			'store_id' 				=> '',
			'external_store_id' 	=> '',
			'automation_id' 		=> 0,
			'custom_fields' 		=> array()
		);

		if( is_array( $saved_options ) ){
			$settings = array_merge( $defaults, $saved_options ); //Merging default settings with saved options

		}else{
			$settings = $defaults;
		}

		if( $value ){ //If a single value should be returned
			
			if( isset( $settings[$value] ) ){ //Checking if value exists
				$settings = $settings[$value];
			}
		}

		return $settings;
	}

	/**
	* Update ActiveCampaign settings
	*
	* @since    10.1
	* @return   array
	* @param    array     $new_value           	New value
	* @param    array     $old_value            Existing value
	*/
	public function handle_settings_update( $new_value, $old_value ){

		if( isset( $_POST['cartbounty_pro_activecampaign_settings'] ) ){ //If option update coming from saving a form
			$new_value = $_POST['cartbounty_pro_activecampaign_settings'];
		}

		if( is_array( $old_value ) && is_array( $new_value ) ){
			$settings = array_merge( $old_value, $new_value );

		}else{
			$settings = $new_value;
		}

		return $settings;
	}

	/**
	 * Method that runs all of the ActiveCampaign business
	 *
	 * @since    5.0
	 */
	public function run(){		
		
		if( $this->api_valid() ){ //If API key is valid
			$this->create_store();
		}
	}
	
	/**
	 * Method that is used for cron synchronization jobs
	 *
	 * @since    5.0
	 */
	public function auto_sync(){
		$admin = $this->admin();

		if( !$admin->check_license() || !class_exists( 'WooCommerce' ) ) return; //Exit if license key not valid or if WooCommerce is not activated

		if( $this->api_valid() && $this->store_connected() ){ //If API key is valid and store is connected
			//Syncing customers which also syncs their shopping carts
			$this->sync_abandoned_carts();
		}
	}

	/**
	 * Method creates an External Store ID or returns the value if it has been already created
	 *
	 * @since    5.0
	 * @return 	 External store ID
	 */
	private function get_external_store_id(){
		$settings = $this->get_settings();
		$external_store_id = $settings['external_store_id'];

		if( empty( $external_store_id ) ){
			$settings['external_store_id'] = get_bloginfo( 'url' );
			update_option( 'cartbounty_pro_activecampaign_settings', $settings );
		}

		return $external_store_id;
	}

	/**
	 * Method retrieves saved API credentials
	 *
	 * @since    5.0
	 * @return 	 Array
	 */
	public function get_api_access(){
		$api_access = array(
			'api_key' => '',
			'api_url' => ''
		);

		$activecampaign_settings = $this->get_settings();
		
		if( !empty( $activecampaign_settings ) ){
			$api_access['api_key'] = esc_attr( $activecampaign_settings['key'] ); //Retrieve ActiveCampaign API key from database
			$api_access['api_url'] = esc_attr( $activecampaign_settings['url'] ); //Retrieve ActiveCampaign API URL from database
		}

		return $api_access;
	}

	/**
	 * Method checks if API is valid
	 * Return True if API valid. Return False if not
	 *
	 * @since    5.0
	 * @return 	 Boolean
	 */
	public function api_valid(){
		$admin = $this->admin();
		$api = $this->api();
		$status = false;
		$api_access = $this->api_access();
		$api_key = $api_access['api_key'];
		$api_url = $api_access['api_url'];

		if( !empty( $api_key ) && !empty( $api_url ) ){ //If ActiveCampaign API fields are not empty

			$current_fingerprint = $admin->get_key_fingerprint( $api_key, $api_url );
			$valid_fingerprint = $admin->get_cartbounty_transient( 'activecampaign_valid_fingerprint' ); //Last known valid fingerprint
			
			if( $valid_fingerprint === $current_fingerprint ){ //If API token and ID have not changed and were already validated before
				$status = true;

			}else{
				$invalid_fp = $admin->get_cartbounty_transient( 'activecampaign_invalid_fingerprint' ); //Last known invalid fingerprint

				if( $invalid_fp !== $current_fingerprint ){ //If API token or ID have changed

					$invalid_until = (int)$admin->get_cartbounty_transient( 'activecampaign_api_invalid_until' );

					if( $invalid_until && time() < $invalid_until && $invalid_fp === $current_fingerprint ){ //Exit and return false if credentials have already failed before and they have not changed
						return $status;
					}

					if( (int)$api->credentials_test( 'activecampaign', $api_access, $this ) ){ //In case we get a valid response from ActiveCampaign
						$admin->set_cartbounty_transient( 'activecampaign_valid_fingerprint', $current_fingerprint, 60*60*48 ); //Cache valid API fingerprint for 48 hours
						$admin->delete_cartbounty_transient( 'activecampaign_api_times_failed' );
						$admin->delete_cartbounty_transient( 'activecampaign_api_invalid_until' );
						$admin->delete_cartbounty_transient( 'activecampaign_invalid_fingerprint' );
						$status = true;

					}else{ //If we have connection issues - try and save response to limit how many times we try to connect
						$fail_count = (int)$admin->get_cartbounty_transient( 'activecampaign_api_times_failed' );
						$fail_count++;
						$admin->set_cartbounty_transient( 'activecampaign_api_times_failed', $fail_count, 60*60 ); //Remember failures for 1 hour

						if( $fail_count >= 3 ){
							$admin->set_cartbounty_transient( 'activecampaign_api_invalid_until', time() + 60*15, 60*15 ); //Invalidate requests for 15 minutes
							$admin->set_cartbounty_transient( 'activecampaign_invalid_fingerprint', $current_fingerprint, 60*15 ); //Cache invalid API fingerprint for 15 minutes
							$admin->delete_cartbounty_transient( 'activecampaign_valid_fingerprint' );
						}
					}
				}
			}
		}

		return $status;
	}
	
	/**
	 * Method returns ActiveCampaign API status
	 *
	 * @since    5.0
	 * @return 	 Array
	 */
	public function api_status(){
		$admin = $this->admin();
		$api_access = $this->api_access();
		$activecampaign_url = $api_access['api_url'];
		$activecampaign_key = $api_access['api_key'];
		$response = array();

		if( !empty( $activecampaign_key ) && !empty( $activecampaign_url ) ){ //If the fields are not empty
			
			if( $this->api_valid() ){
				$response['status'] = '1';
				$response['result'] = '';
				$response['message'] =  esc_html__( 'Connected', 'woo-save-abandoned-carts' );

			}else{
				$response['status'] = '0';
				$response['result'] = esc_html__( 'Error', 'woo-save-abandoned-carts' );
				$response['message'] =  esc_html__( 'Please make sure your API details are correct', 'woo-save-abandoned-carts' );
				$this->delete_store( $store_id = false );
			}

		}else{ //In case the ActiveCampaign API and URL fields are empty
			$response['status'] = '0';
			$response['result'] = '';
			$response['message'] = '';
			$this->delete_store( $store_id = false );
			$admin->delete_cartbounty_transient( 'activecampaign_api_times_failed' );
			$admin->delete_cartbounty_transient( 'activecampaign_api_invalid_until' );
			$admin->delete_cartbounty_transient( 'activecampaign_valid_fingerprint' );
			$admin->delete_cartbounty_transient( 'activecampaign_invalid_fingerprint' );
		}

		return $response;
	}

	/**
	 * Method creates a store on ActiveCapmaign
	 *
	 * @since    5.0
	 */
	private function create_store(){
		$admin = $this->admin();
		$api = $this->api();
		$external_store_id = $this->get_external_store_id();

		if( !$this->store_connected() ){ //If store is not connected we go ahead and create a new connection
			
			if( !$this->check_existing_store() ){ //If current store is not linked
				
				if( !$this->link_existing_store() ){ //Try to link back current store from ActiveCampaign
					//Creating a new store connection on ActiveCampaign since we couln'd find any that would match our store
					$response = '';
					$data['connection'] = array(
						'service' 		=> CARTBOUNTY_PRO_PLUGIN_NAME,
						'externalid' 	=> $external_store_id,
						'name' 			=> get_bloginfo( 'name' ),
						'logoUrl' 		=> CARTBOUNTY_PRO_LOGO_70_LINK,
						'linkUrl' 		=> get_bloginfo( 'url' )
					);
					
					$result = $api->connect( 'activecampaign', $this->api_access(), 'connections', 'POST', $data );					

					if( !empty( $result['response'] ) ){
						$response = $result['response'];
					}

					if( $result['status_code'] == 200 || $result['status_code'] == 201 ){
						$connection = $response->connection;
						
						if( isset( $connection->id ) ){ //If we could create a store, then it means, saving store ID in our database
							$settings = $this->get_settings();
							$settings['store_id'] = $connection->id;
							update_option( 'cartbounty_pro_activecampaign_settings', $settings );
							$admin->set_cartbounty_transient( 'activecampaign_external_store_id', $external_store_id, 60*60*48 ); //Cache Store ID for 48 hours
							$admin->log( 'info', 'ActiveCampaign: Successful store creation.' );
						}

					}else{ //We were not able to create a store

						if( isset( $response->errors ) ){
							$this->log_ac_errors( $response->errors );
						}
						$admin->log( 'info', 'ActiveCampaign: Unable to create a store.' );
					}
				}
			}
		}
	}

	/**
	 * Method checks if current store is connected to ActiveCampaign
	 *
	 * @since    5.0
	 * @return   Boolean
	 */
	public function store_connected(){
		$admin = $this->admin();
		$api = $this->api();
		$connected = false;
		$external_store_id = $this->get_external_store_id();
		$cached_activecampaign_external_store_id = $admin->get_cartbounty_transient( 'activecampaign_external_store_id' );

		if( $this->api_valid() && $cached_activecampaign_external_store_id == $external_store_id ){ //If we have a valid API key and temporary store ID is equal with the saved store ID - store connected
			$connected = true;

		}elseif( $this->api_valid() ){ //Connecting to ActiveCampaign to get response about a connected store
			$response = '';
			$settings = $this->get_settings();
			$store_id = $settings['store_id'];
			$result = $api->connect( 'activecampaign', $this->api_access(), 'connections/' . $store_id, 'GET' ); //Retrieving store ID from ActiveCampaign

			if( !empty( $result['response'] ) ){
				$response = $result['response'];
			}

			if( $result['status_code'] == 200 ){ //If we get back a valid list ID
				
				if( !empty( $response->connection ) ){
					$connection = $response->connection;
					$externalid = $connection->externalid;
					
					if( $externalid == $external_store_id ){ //If our external store ID is equal with the one that is received from ActiveCampaign, we have a successful connection
						$admin->set_cartbounty_transient( 'activecampaign_external_store_id', $external_store_id, 60*60*48 ); //Cache Store ID for 48 hours
						$connected = true;
					}
				}

			}else{ //Trying to log errors

				$error = '';

				if( isset( $response->errors ) ){
					$error = $response->errors;

				}elseif( isset( $response->message ) ){
					$error = $response->message;
				}

				if( !empty( $error ) ){
					$this->log_ac_errors( $error );
				}
			}
		}

		return $connected;
	}

	/**
	 * Method checks if current store exists on ActiveCampaign and returns True / False
	 *
	 * @since    5.0
	 * @return   Boolean
	 */
	private function check_existing_store(){
		$admin = $this->admin();
		$api = $this->api();
		$store_exists = false;
		$response = '';
		$store_id = $this->get_settings( 'store_id' );

		if( $store_id ){ //If we have saved previous ActiveCampaign's store ID
			$result = $api->connect( 'activecampaign', $this->api_access(), 'connections/' . $store_id, 'GET' ); //Retrieving connections from ActiveCampaign

			if( !empty( $result['response'] ) ){
				$response = $result['response'];
			}

			if( $result['status_code'] == 200 ){ //If we have found a connection
				$store = $response->connection;
				
				if( isset( $store->externalid ) ){
					$external_store_id = $this->get_external_store_id();
					
					if( $external_store_id == $store->externalid ){ //If we find a match, it means that we have found our store
						$admin->set_cartbounty_transient( 'activecampaign_external_store_id', $external_store_id, 60*60*48 ); //Cache Store ID for 48 hours
						$admin->log( 'info', 'ActiveCampaign: Store exists and is linked.' );
						$store_exists = true;
					}
				}

			}else{
				$error = '';

				if( isset( $response->errors ) ){
					$error = $response->errors;

				}elseif( isset( $response->message ) ){
					$error = $response->message;
				}

				if( !empty( $error ) ){
					$this->log_ac_errors( $error );
				}
			}
		}

		return $store_exists;
	}

	/**
	 * Method looks for all store connections on ActiveCampaign and tries to link it back if it finds it and returns True/False
	 *
	 * @since    5.0
	 * @return   Boolean
	 */
	private function link_existing_store(){
		$admin = $this->admin();
		$api = $this->api();
		$response = '';
		$store_linked = false;
		$data = array(
			'limit' => 200,
		);
		$result = $api->connect( 'activecampaign', $this->api_access(), 'connections', 'GET', $data ); //Retrieving connections from ActiveCampaign

		if( !empty( $result['response'] ) ){
			$response = $result['response'];
		}

		if( $result['status_code'] == 200 ){
			$connections = $response->connections;
			$external_store_id = $this->get_external_store_id();

			if( !empty( $connections ) ){ //If we have some connections
				foreach( $connections as $key => $connection ){ //Looping through retrieved connections
					
					if( $external_store_id == $connection->externalid ){ //If we find an exiting connection, link it
						$settings = $this->get_settings();
						$settings['store_id'] = $connection->id;
						update_option( 'cartbounty_pro_activecampaign_settings', $settings ); //Updating our store id so that it matches ActiveCampaign's id value
						$admin->set_cartbounty_transient( 'activecampaign_external_store_id', $external_store_id, 60*60*48 ); //Cache Store ID for 48 hours
						$admin->log( 'info', 'ActiveCampaign: Store successfully found and linked back.' );
						$store_linked = true;
					}
				}

			}elseif( isset( $response->errors ) ){
				$this->log_ac_errors( $response->errors );
			}
		}

		return $store_linked;
	}

	/**
	 * Method disconnects a Store from ActiveCampaign
	 *
	 * @since    5.0
	 */
	public function delete_store( $store_id ){
		$admin = $this->admin();
		$api = $this->api();
		$settings = $this->get_settings();

		if( !$store_id ){ //If functions has not passed a store ID
			$store_id = $settings['store_id']; //Loading connected store id we have on ActiveCampaign
		}

		if( $store_id ){
			$api->connect( 'activecampaign', $this->api_access(), 'connections/' . $store_id, 'DELETE' ); //Deleting store
			$settings['store_id'] = '';
			$settings['external_store_id'] = '';
			$admin->delete_cartbounty_transient( 'activecampaign_external_store_id' ); //Removing cached API token data
			update_option( 'cartbounty_pro_activecampaign_settings', $settings );
		}
	}

	/**
	* Function synchronizes abandoned carts to ActiveCampaign
	* Customers without email address are not synced to ActiveCampaign
	* While ActiveCampaign v3 API doesn't allow asynchronous requests, we are forced to limit our syncronisations to 2 abandoned cart rows each time since more rows take a lot of time to be synced
	*
	* @since    5.0
	*/
	private function sync_abandoned_carts(){
		global $wpdb;
		$admin = $this->admin();
		$api = $this->api();

		if( !$admin->check_license() ) return; //Exit if license key not valid

		$cart_table = $wpdb->prefix . CARTBOUNTY_PRO_TABLE_NAME;
		$time = $admin->get_time_intervals();
		$email_consent_query = '';

		if( $admin->get_consent_settings( 'email' ) ){ //Check if email consent enabled to query only carts with given email usage consent
			$email_consent_query = 'AND email_consent = 1';
		}

		$customers_to_activecampaign = $wpdb->get_results(
			$wpdb->prepare(
				"SELECT id, name, surname, email, phone, cart_contents, cart_total, session_id, time, ac_last_synced, ac_customer_id, ac_cart_id
				FROM {$cart_table}
				WHERE time != ac_last_synced AND
				(type = %d OR type = %d) AND
				email != '' AND
				cart_contents != '' AND
				paused != 1 AND
				ac_excluded != 1 AND
				anonymized != 1 AND
				time < %s AND
				time > %s
				$email_consent_query
				ORDER BY id DESC",
				$admin->get_cart_type( 'abandoned' ),
				$admin->get_cart_type( 'recovered_pending' ),
				$time['ac_cart_sync_period'],
				$time['maximum_sync_period']
			)
		);

		if( $customers_to_activecampaign ){ //If we have new customers
			$limit = 0;
			foreach( $customers_to_activecampaign as $key => $customer ){

				if( $limit > 1 ){ //Once we have synced 2 abandoned carts, stopping sync. No more than 2 abandoned carts per sync due to speed limitations
					break;
				}

				if( !$admin->has_excluded_items( $customer, 'activecampaign' ) ){ //If cart contents do not have excluded items
					
					if( $customer->ac_cart_id == 0 ){ //If ac_customer_id haven't been entered - the row hasn't been synced ever and we must create a new customer and abandoned cart

						$response = '';
						$result = array( 'status_code' => '' );
						$store_id = $this->get_settings( 'store_id' );
						$this->update_contact( $customer->id, $customer->name, $customer->surname, $customer->email, $customer->phone ); //Create or update contact

						//Create customer on ActiveCampaign. Ignore customer creation if it already exists
						//Preparing customer data array
						$data['ecomCustomer'] = array(
							'connectionid' 		=> $store_id,
							'externalid' 		=> $customer->id,
							'email' 			=> $customer->email,
							'acceptsMarketing' 	=> 0
						);

						if( $customer->ac_customer_id == 0 ){ //If this is the first time we are trying to sync this customer to ActiveCampaign
							$result = $api->connect( 'activecampaign', $this->api_access(), 'ecomCustomers', 'POST', $data ); //Creating a new customer on ActiveCampaign

							if( !empty( $result['response'] ) ){
								$response = $result['response'];
							}
						}

						if( $result['status_code'] == 201 ){  //Check if we have a valid response and update our row
							//Get and update customers ID so that we can use it later for syncing abandoned cart
							$customer_result = $response->ecomCustomer;
							$wpdb->query(
								$wpdb->prepare( "UPDATE {$cart_table} 
								SET ac_customer_id = %s
								WHERE id = %d", $customer_result->id, $customer->id )
							);

							//Create abandoned order data on ActiveCampaign
							$this->sync_carts( $customer->id, $customer_result->id, $customer->ac_cart_id, $customer->name, $customer->surname, $customer->email, $customer->phone, $customer->cart_contents, $customer->cart_total, $customer->session_id, $customer->time, $customer->ac_last_synced, $action = "POST", $customer );
							$limit++;

						}else{//If we fail to create a new customer or the customer already has been previously created
							$ac_customer_id = $customer->ac_customer_id;
							if( $ac_customer_id == 0 ){ // In case if the current row doesn't have customer's ID
							//Try to find customers ID in local DB
								$ac_customer_id = $wpdb->get_var(
									$wpdb->prepare(
										"SELECT ac_customer_id FROM $cart_table
										WHERE email = %s AND ac_customer_id != 0
										ORDER BY id DESC", $customer->email
									)
								);
							}

							if( $ac_customer_id ){ //If we have customer's ID - create a new shopping cart for this customer
								if( $customer->ac_customer_id == 0 ){ //If current abandoned cart row doesn't have customer ID
									//Add customers ID to the current row
									$wpdb->query(
										$wpdb->prepare( "UPDATE {$cart_table} 
										SET ac_customer_id = %s
										WHERE id = %d", $ac_customer_id, $customer->id )
									);
								}

								//Create abandoned order data on ActiveCampaign
								$this->sync_carts( $customer->id, $ac_customer_id, $customer->ac_cart_id, $customer->name, $customer->surname, $customer->email, $customer->phone, $customer->cart_contents, $customer->cart_total, $customer->session_id, $customer->time, $customer->ac_last_synced, $action = "POST", $customer );
								$limit++;

							}else{ //If we are not able to get customer's ID then it means that the customer no longer exists in the local database but exists on ActiveCampaign
								//Asking ActiveCampaign for customer's ID using an email filter and limiting response to 1 result
								$response = '';
								$data = array(
									'filters' 	=> array(
										'email' 		=> $customer->email,
										'connectionid' 	=> $store_id
									),
									'limit' 	=> 1
								);
								$result = $api->connect( 'activecampaign', $this->api_access(), 'ecomCustomers', 'GET', $data );

								if( !empty( $result['response'] ) ){
									$response = $result['response'];
								}

								if( $result['status_code'] == 200 ){ //If we get a positive response from ActiveCampaign
									$ecomCustomers = $response->ecomCustomers;

									if( !empty( $ecomCustomers ) ){ //If we have at least one customer in the array
										$ecomCustomer = $ecomCustomers[0]; //Picking up first customer's ID from the array

										if( $customer->ac_customer_id == 0 ){ //If current abandoned cart row doesn't have customer ID
											//Add customers ID to the current row
											$wpdb->query(
												$wpdb->prepare( "UPDATE {$cart_table} 
												SET ac_customer_id = %s
												WHERE id = %d", $ecomCustomer->id, $customer->id )
											);
										}

										//Create abandoned order data on ActiveCampaign
										$this->sync_carts( $customer->id, $ecomCustomer->id, $customer->ac_cart_id, $customer->name, $customer->surname, $customer->email, $customer->phone, $customer->cart_contents, $customer->cart_total, $customer->session_id, $customer->time, $customer->ac_last_synced, $action = "POST", $customer );
										$limit++;

									}else{ //In case if we fail to get any customer from ActiveCampaign. Updating row so it wouldn't sync anymore and log event
										$wpdb->query(
											$wpdb->prepare("UPDATE {$cart_table}
											SET ac_last_synced = %s
											WHERE id = %d", $customer->time, $customer->id)
										);

										$this->log_ac_errors( esc_html__( "Seems like we are not able to sync this cart. Please delete Contact with the following email from ActiveCampaign to fix the issue.", 'woo-save-abandoned-carts' ), $extra = array(
												'email' => $customer->email
										) );
									}
								}
							}
						}

					}else{ //The abandoned cart row has previously already been synced over to ActiveCampaign. Updating the cart on ActiveCampaign
						$this->sync_carts( $customer->id, $customer->ac_customer_id, $customer->ac_cart_id, $customer->name, $customer->surname, $customer->email, $customer->phone, $customer->cart_contents, $customer->cart_total, $customer->session_id, $customer->time, $customer->ac_last_synced, $action = "PUT", $customer );
						$limit++;
					}

				}else{
					$admin->exclude_cart( 'activecampaign', $customer ); //Exclude cart from further recovery
				}
			}

			return;
		}
		return;
		
	}

	/**
	 * Method synchronizes generated coupon codes to ActiveCampaign as custom fields
	 * First checking if custom fields themselves have been created on ActiveCampaign - create them in case they do not exist
	 * Then sync custom fields to ActiveCampaign once the carts and contacts have been synced
	 *
	 * @since    9.4
	 * @param 	 array    $cart_id    	Abandoned cart ID coupon of which must be synced over to ActiveCampaign
	 */
	private function sync_custom_fields( $cart_id ){
		global $wpdb;
		$admin = $this->admin();
		$coupons = $this->coupons();
		$api = $this->api();
		
		if( !$admin->check_license() ) return; //Exit if license key not valid

		$coupon_enabled = $coupons->coupon_enabled( 'activecampaign', 'cartbounty_pro_activecampaign_coupon', $step_nr = false );
		$settings = $this->get_settings();

		if( !$coupon_enabled ){ //If coupon disabled - delete custom fields from ActiveCampaign and stop sync
			$settings['custom_fields'] = array();
			update_option( 'cartbounty_pro_activecampaign_settings', $settings );
			return;
		}

		if( empty( $settings['custom_fields'] ) ){ //Check if custom fields have been created over at ActiveCampaign
			$created = $this->create_coupon_code_custom_field();
			if( !$created ){ //In case the custom fields not created, exit coupon sync
				return;
			}
		}

		$cart_table = $wpdb->prefix . CARTBOUNTY_PRO_TABLE_NAME;
		$cart_row = $wpdb->get_row(
			$wpdb->prepare(
				"SELECT ac_contact_id, coupons
				FROM {$cart_table}
				WHERE id = %d",
				$cart_id
			)
		);
		
		if( $cart_row ){ //If we have a cart with unsynced coupon
			$coupon_data = maybe_unserialize( $cart_row->coupons );
			$coupon_code = '';
			$custom_fields = $settings['custom_fields'];

			if( isset( $coupon_data['activecampaign'] ) ){ //If a coupon for ActiveCampaign exists
				$coupon_code = $coupon_data['activecampaign'];
			}

			if( is_array( $custom_fields ) ){
				
				foreach( $custom_fields as $key => $custom_field ){
					$data = array();

					if( $custom_field['tag'] == 'CBCOUPON' ){
						$data['fieldValue'] = array(
							'contact' 	=> $cart_row->ac_contact_id,
							'field' 	=> $custom_field['id'],
							'value' 	=> strtoupper( $coupon_code )
						);
					}

					if( $custom_field['tag'] == 'CBCOUPONXP' ){
						$coupon_expiration_date = $coupons->get_coupon_expiration_date( $coupon_code );
						
						if( empty( $coupon_expiration_date ) ){ //If expiration date not set
							$coupon_expiration_date = '';
						}

						$data['fieldValue'] = array(
							'contact' 	=> $cart_row->ac_contact_id,
							'field' 	=> $custom_field['id'],
							'value' 	=> $coupon_expiration_date,
						);
					}

					if( !empty( $data ) ){
						$api->connect( 'activecampaign', $this->api_access(), 'fieldValues', 'POST', $data );
					}
				}
			}
		}
	}

	/**
	* Function creates new or updates exiting contact on ActiveCampaign and return contact data
	*
	* @since    5.0
	*/
	private function update_contact( $id, $name, $surname, $email, $phone ){
		$api = $this->api();
		$response = '';
		$data['contact'] = array(
			'email' 		=> $email,
			'firstName' 	=> sanitize_text_field( wp_unslash( (string)( $name ?? '' ) ) ),
			'lastName' 		=> sanitize_text_field( wp_unslash( (string)( $surname ?? '' ) ) ),
			'phone' 		=> $phone
		);
		
		$result = $api->connect( 'activecampaign', $this->api_access(), 'contact/sync', 'POST', $data ); //Creating or updating contact data

		if( !empty( $result['response'] ) ){
			$response = $result['response'];
		}

		if( $result['status_code'] == 200 ){
			$contact = $response->contact;

			if( isset( $contact->id ) ){ //If we have received contact's ID, update it in our DB
				global $wpdb;
				$cart_table = $wpdb->prefix . CARTBOUNTY_PRO_TABLE_NAME;
				$contact_id = $contact->id;
				$wpdb->query(
					$wpdb->prepare( "UPDATE {$cart_table}
					SET ac_contact_id = %d
					WHERE id = %d", $contact_id, $id )
				);
			}
		}
	}

	/**
	* Function synchronizes abandoned carts to ActiveCampaign. Product variations are not supported by ActiveCampaign at this point.
	*
	* @since    5.0
	*/
	private function sync_carts( $id, $ac_customer_id, $ac_cart_id, $name, $surname, $email, $phone, $cart_contents, $cart_total, $session_id, $time, $ac_last_synced, $action, $cart = false ){
		$admin = $this->admin();
		$coupons = $this->coupons();
		$api = $this->api();

		if( !$admin->check_license() ) return; //Exit if license key not valid

		global $wpdb;
		$cart_table = $wpdb->prefix . CARTBOUNTY_PRO_TABLE_NAME;
		$checkout_url = $admin->create_cart_url( $email, $session_id, $id, 'activecampaign' );
		$date_object = new DateTime( $time );
		$abandonedDate = $date_object->format( 'c' );

		$coupon = $coupons->get_coupon( 'activecampaign', 'cartbounty_pro_activecampaign_coupon', $step_nr = false, (object)$cart ); //Coupon code creation
		
		$lines = $this->prepare_lines( $cart_contents ); //Creating lines from cart contents

		//Preparing customer data array
		$data['ecomOrder'] = array(
			'externalcheckoutid' 	=> $id,
			'source' 				=> 1,
			'email' 				=> $email,
			'orderProducts' 		=> $lines,
			'totalPrice'			=> $cart_total * 100,
			'currency' 				=> get_woocommerce_currency(),
			'connectionid' 			=> $this->get_settings( 'store_id' ),
			'customerid' 			=> $ac_customer_id,
			'orderUrl' 				=> $checkout_url,
			'abandonedDate' 		=> $abandonedDate,
			'externalCreatedDate' 	=> $abandonedDate
		);

		if( $action == 'POST' ){
			$response = '';
			$result = $api->connect( 'activecampaign', $this->api_access(), 'ecomOrders', $action, $data ); //Sending customer data over to ActiveCampaign to Create a new cart for the customer

			if( !empty( $result['response'] ) ){
				$response = $result['response'];
			}

			if( $result['status_code'] == 201 ){ //Check if we have a valid response and update both the time and ActiveCampaign cart id value
				$cart_result = $response->ecomOrder;
				$wpdb->query(
					$wpdb->prepare( "UPDATE {$cart_table}
					SET ac_cart_id = %d, ac_last_synced = %s
					WHERE id = %d", $cart_result->id, $time, $id )
				);

			}else{

				$error = '';

				if( isset( $response->error ) ){
					$error = $response->error;

				}elseif( isset( $response->errors ) ){
					$error = $response->errors;
				}

				if( !empty( $error ) ){
					$this->log_ac_errors( $error, $extra = array(
						'id' 		=> $id,
						'email' 	=> $email
					) );
				}

				$admin->exclude_cart( 'activecampaign', $cart );
			}

		}elseif( $action == 'PUT' ){
			$api->connect( 'activecampaign', $this->api_access(), 'ecomOrders/' . $ac_cart_id, $action, $data ); //Sending customer data over to ActiveCampaign
			//In case of cart update only update ac_last_synced to match the update time
			$wpdb->query(
				$wpdb->prepare( "UPDATE {$cart_table}
				SET ac_last_synced = %s
				WHERE id = %d", $time, $id )
			);
		}

		$this->sync_custom_fields( $id ); //Sync custom fields once the cart has been synced
		return;
	}

	/**
	 * Method sends an update request of a given shopping cart to ActiveCampaign to convert abandoned cart into an order and show that it has been recovered
	 * Method is sending update request only if the cart has been synced
	 *
	 * @since    6.4
	 * @param    object     $cart            Abandoned cart that has just been turned into order
	 */
	public function sync_order( $cart ){

		if( !$this->store_connected() ) return;

		if( !$this->cart_is_synced( $cart ) ) return;

		$api = $this->api();
		$response = '';
		$lines = $this->prepare_lines( $cart->cart_contents ); //Creating lines from cart contents			
		$date_object = new DateTime( $cart->time );
		$abandonedDate = $date_object->format( 'c' );

		$data['ecomOrder'] = array(
			'externalid' 			=> $cart->id,
			'externalcheckoutid' 	=> $cart->id,
			'source'				=> 1,
			'email' 				=> $cart->email,
			'orderProducts' 		=> $lines,
			'totalPrice' 			=> $cart->cart_total * 100,
			'currency' 				=> get_woocommerce_currency(),
			'connectionid' 			=> $this->get_settings( 'store_id' ),
			'customerid' 			=> $cart->ac_customer_id,
			'externalCreatedDate' 	=> $abandonedDate
		);
		$result = $api->connect( 'activecampaign', $this->api_access(), 'ecomOrders/' . $cart->ac_cart_id, 'PUT', $data );

		if( !empty( $result['response'] ) ){
			$response = $result['response'];
		}

		if( $result['status_code'] != 200 ){ //If we encounter errors

			$error = '';

			if( isset( $response->errors ) ){
				$error = $response->errors;

			}elseif( isset( $response->message ) ){
				$error = $response->message;
			}

			if( !empty( $error ) ){
				$this->log_ac_errors( $error, $extra = array(
					'id' 		=> $cart->id,
					'email' 	=> $cart->email
				) );
			}
		}

		$this->remove_contact_from_automation( $cart->ac_contact_id );
	}

	/**
	 * Method prepares product lines
	 *
	 * @since    6.4
	 * @param    array       $cart_contents				Abandoned cart contents
	 */
	private function prepare_lines( $cart_contents ){
		$admin = $this->admin();
		$cart_contents = $admin->get_saved_cart_contents( $cart_contents );
		
		if( is_array( $cart_contents ) ){
			$line_id = 1;
			$lines = array();

			foreach( $cart_contents as $cart_product ){ //Handling cart contents
				$product_id = $cart_product['product_id']; //Product ID
				$inventory_quantity = $cart_product['quantity']; //Quantity
				$product = wc_get_product( $product_id ); //Getting product data

				if( $product ){ //If we have a product
					$title = strip_tags( $product->get_name() );
					$url = get_permalink( $product->get_id() );
					$description = $product->get_short_description();
					$categories = strip_tags( wc_get_product_category_list( $product_id ) );
					$product_price = $admin->get_product_price( $cart_product );
					$image_url = $admin->get_product_thumbnail_url( $cart_product );

					$line = array(
						'name' 			=> $title,
						'price' 		=> $product_price * 100,
						'quantity' 		=> $inventory_quantity,
						'externalid' 	=> $product_id,
						'category' 		=> $categories,
						'description' 	=> $description,
						'productUrl' 	=> $url,
						'imageUrl' 		=> $image_url
					);
					$lines[] = $line;
					$line_id++;

				}else{
					$admin->log( 'notice', sprintf( 'ActiveCampaign: WooCommerce product does not exist. ID: %d', esc_html( $product_id ) ) );
				}
			}

			return $lines;
		}
	}

	/**
	 * Method Checks if current abandoned cart is synced
	 *
	 * @since    5.0
	 * @return   boolean
	 * @param    object     $cart            Abandoned cart data
	 */
	public function cart_is_synced( $cart ){
		$synced = false;

		if( !empty( $cart ) ){
			if( isset( $cart->ac_cart_id ) ){
				if( $cart->ac_cart_id ){
					$synced = true;
				}
			}
		}

		return $synced;
	}

	/**
	 * Method sends a request to remove a specific contact from ActiveCampaign automation
	 *
	 * @since    9.2.3
	 * @param 	 number    $contact_id    	Contact's ID from the database. Default false
	 */
	public function remove_contact_from_automation( $contact_id = false ){
		
		if( !$this->store_connected() ) return;

		global $wpdb;
		$api = $this->api();
		$response = '';
		$selected_automation_id = $this->get_settings( 'automation_id' );

		if( !$contact_id || !$selected_automation_id ) return; //If contact's ID not present or no automation selected, exit

		$data = array(
			'limit' => 200
		);

		$result = $api->connect( 'activecampaign', $this->api_access(), 'contacts/' . $contact_id . '/contactAutomations', 'GET', $data ); //Get a max of 200 automations a contact is in or has been in

		if( !empty( $result['response'] ) ){
			$response = $result['response'];
		}

		if( $result['status_code'] == 200 ){
			$contactAutomations = $response->contactAutomations;
				krsort( $contactAutomations ); //Sort array descending by the key since the scheduled automations will be at the end
				foreach( $contactAutomations as $key => $automation ){
					
					if( $automation->automation == $selected_automation_id && $automation->batchid == 'scheduled' ){ //If we find automation that is scheduled (not finished) and it matches the automation we have selected in the settings
						$automation_id = $automation->id; //The ID of the automation the contact must be removed from
						break; //End foreach as soon as we find the first matching value
					}
				}

				if( isset( $automation_id ) ){
					$api->connect( 'activecampaign', $this->api_access(), 'contactAutomations/' . $automation_id, 'DELETE' ); //Remove contact from automation
				}

		}else{ //If contact automations were not received
			
			if( isset( $response->message ) ){
				$this->log_ac_errors( $response->message . esc_html__( ' (Contact automations)', 'woo-save-abandoned-carts' ) );
			}
		}
	}

	/**
	 * Send a request to ActiveCampaign not to send reminders about duplicate carts
	 * Since contact removal request was already sent after a successful order and that automatically removes all abandoned carts for the customer from automation - do not do anything. Function currently serves as a placeholder for future development.
	 *
	 * @since    9.5
	 * @param    array      $carts          Cart array
	 */
	public function delete_duplicate_carts( $carts ){
		return;
	}

	/**
	 * Method returns abandoned cart ActiveCampaign contact ID from cart ID
	 *
	 * @since    9.2.3
	 * @return 	 Integer or False
	 * @param    integer     $id - Abandoned cart ID
	 */
	public function get_ac_contact_id( $id ){
		global $wpdb;
        $cart_table = $wpdb->prefix . CARTBOUNTY_PRO_TABLE_NAME;
        $ac_contact_id = $wpdb->get_var(
            $wpdb->prepare(
                "SELECT ac_contact_id FROM $cart_table
                WHERE id = %d",
                intval( $id )
            )
        );
        return $ac_contact_id;
    }

    /**
	 * Method that retrieves and outputs existing automations from ActiveCampaign
	 * Required because we are sending contact removal requests from automations to selected automation. 
	 * If it has not been selected, we are unable to remove contact from automation in case of a complete order
	 *
	 * @since    9.2.3
	 */
	public function display_automations(){
		$admin = $this->admin();
		$api = $this->api();
		$response = '';
		$all_automations = array();
		$limit = 100; //Maximum number ActiveCampaign currently allows to retrieve
		$retrieved_automations = 0;

		//In case we have more than 100 automations, use offset pagination to retreave all of them
		do{
			$data = array(
				'limit' 	=> $limit,
				'offset' 	=> $retrieved_automations,
			);

			$result = $api->connect( 'activecampaign', $this->api_access(), 'automations', 'GET', $data );

			if( !empty( $result['response'] ) ){
				$response = $result['response'];
				$automations = $response->automations;
				$retrieved_automations += count( $automations );
				$all_automations = array_merge( $all_automations, $automations );
			}

		}while( count( $automations ) == $limit ); //Continue if 100 automations were retrieved (indicating there may be more)
		
		if( $result['status_code'] == 200 ){
			$automations = $all_automations;
			$automation_id = $this->get_settings( 'automation_id' );
			$action = '';
		?>
			<select id="cartbounty_pro_activecampaign_automation_id" class="cartbounty-pro-select" name="cartbounty_pro_activecampaign_settings[automation_id]" <?php echo $admin->disable_field( $action ); ?> autocomplete="off">
				<option value="0" <?php echo selected( $automation_id, 0 ) ?>><?php esc_html_e( 'Choose an automation', 'woo-save-abandoned-carts' );?></option>
				<?php foreach( $automations as $automation ): ?>
					<option value="<?php echo esc_attr( $automation->id ) ?>" <?php selected( $automation_id, $automation->id ); ?> > <?php echo esc_html( $automation->name ); ?> </option>
				<?php endforeach; ?>
			</select>
		<?php
		}else{ ?>
			<select id="cartbounty_pro_activecampaign_automation_id" class="cartbounty-pro-select" disabled>
				<option><?php esc_html_e( 'No automations available', 'woo-save-abandoned-carts' );?></option>
			</select>
			<p class='cartbounty-pro-additional-information'>
				<?php esc_html_e( 'Please create your first automation on ActiveCampaign.', 'woo-save-abandoned-carts' ); ?>
			</p>
		<?php
		}
	}

	/**
	* Function handles ActiveCampaign error array
	*
	* @since    5.0
	* @param 	array     $errors - ActiveCampaign error array
	* @param 	array     $extra - additional information to output, optional
	*/
	public function log_ac_errors( $errors, $extra = array() ){
		$admin = $this->admin();
		$id = '';
		$email = '';

		if( isset( $extra['id'] ) ){
			$id = 'Cart: ' . $extra['id'] . '.';
		}

		if( isset( $extra['email'] ) ){
			$email = 'Email: ' . $extra['email'] . '.';
		}
		
		if( is_array( $errors ) ){
			foreach( $errors as $key => $error ){
				$admin->log( 'notice', sprintf( 'ActiveCampaign: %s %s %s', esc_html( $error->title ), esc_html( $email ), esc_html( $id ) ) );
			}
		}else{
			$admin->log( 'notice', sprintf( 'ActiveCampaign: %s %s %s', esc_html( $errors ), esc_html( $email ), esc_html( $id ) ) );
		}
	}

	/**
	 * Method creates custom fields on ActiveCampaign. Returns True in case fields successfully created
	 *
	 * @since    9.4
	 * @return   boolean
	 */
	private function create_coupon_code_custom_field(){
		$admin = $this->admin();
		$api = $this->api();
		$created = false;
		$custom_field_data = array();
		$response = '';

		if( !$admin->check_license() ) return; //Exit if license key not valid

		if( $this->store_connected() ){ //If ActiveCampaign is connected
			$custom_fields = array(
				array(
					'field' => array(
						'title' 	=> 'CartBounty coupon',
						'type' 		=> 'text',
						'perstag' 	=> 'CBCOUPON'
					)
				),
				array(
					'field' => array(
						'title' 	=> 'CartBounty coupon expiration',
						'type' 		=> 'date',
						'perstag' 	=> 'CBCOUPONXP'
					)
				)
			);

			foreach( $custom_fields as $key => $custom_field ){
				$result = $api->connect( 'activecampaign', $this->api_access(), 'fields', 'POST', $custom_field ); //Creating custom fields on ActiveCampaign

				if( !empty( $result['response'] ) ){
					$response = $result['response'];
				}

				if( $result['status_code'] == 201 ){ //If we have been able to create the new field
					$field = $response->field;
					$data = array(
						'fieldRel' => array(
							'field' 	=> $field->id
						)
					);
					$api->connect( 'activecampaign', $this->api_access(), 'fieldRels', 'POST', $data ); //Add custom field to be visible in all lists
					$custom_field_data[] = array( 
						'id' 	=> $field->id,
						'tag' 	=> $custom_field['field']['perstag']
					);
				}
			}

			if( count( $custom_fields ) == count( $custom_field_data ) ){ //If all sent custom fields have been successfully created
				$settings = $this->get_settings();
				$settings['custom_fields'] = $custom_field_data;
				update_option( 'cartbounty_pro_activecampaign_settings', $settings );
				$created = true;

			}else{
				//This is created only to assist in case if it is unable to create all custom fields
				//Once 9.9.2 version was released, we have introduced a new options field which reuqires other data
				//It can create a conflict with previous data so we are deleting previous custom field values
				$restored = $this->try_restore_coupon_code_custom_fields( count( $custom_fields ) );

				if( !$restored ){ //If custom field restore was not successful - delete custom field data
					$this->delete_coupon_code_custom_fields();
				}
			}
		}

		return $created;
	}

	/**
	 * Trying to restore custom fields from ActiveCampaign in case they have already been created before
	 * Restoring only in case all custom fields exist on ActiveCampaign
	 *
	 * @since    9.4
	 * @return   boolean
	 * @param    integer     $field_count             Count of fields we are looking to restore
	 */
	private function try_restore_coupon_code_custom_fields( $field_count ){
		$restored = false;
		$custom_fields_from_activecampaign = $this->get_cartbounty_custom_fields_from_activecampaign();

		if( is_array( $custom_fields_from_activecampaign ) ){
			
			if( $field_count == count( $custom_fields_from_activecampaign ) ){ //If all custom fields exist on ActiveCampaign
				$settings = $this->get_settings();
				$settings['custom_fields'] = $custom_fields_from_activecampaign;
				update_option( 'cartbounty_pro_activecampaign_settings', $settings );
				$restored = true;
			}
		}

		return $restored;
	}

	/**
	 * Get custom fields from ActiveCampaign and send a delete request to ActiveCampaign if there are any CartBounty fields
	 *
	 * @since    9.4
	 */
	private function delete_coupon_code_custom_fields(){
		$api = $this->api();
		$custom_fields_from_activecampaign = $this->get_cartbounty_custom_fields_from_activecampaign();

		foreach( $custom_fields_from_activecampaign as $key => $custom_field ){
			$api->connect( 'activecampaign', $this->api_access(), 'fields/'. $custom_field['id'], 'DELETE' );
		}
		$settings = $this->get_settings();
		$settings['custom_fields'] = array();
		update_option( 'cartbounty_pro_activecampaign_settings', $settings );
	}

	/**
	* Get a list of CartBounty custom fields from ActiveCampaign
	*
	* @since    9.9.2
	* @return   Array
	*/
	private function get_cartbounty_custom_fields_from_activecampaign(){
		$api = $this->api();
		$response = '';
		$custom_fields_from_activecampaign = array();
		$cartbounty_custom_fields = array( 'CBCOUPON', 'CBCOUPONXP' );
		$result = $api->connect( 'activecampaign', $this->api_access(), 'fields', 'GET' );

		if( !empty( $result['response'] ) ){
			$response = $result['response'];
		}

		if( $result['status_code'] == 200 ){ //If custom fields sucessfuly found - loop through to find CartBounty coupon fields that must be restored

			if( is_array( $response->fields ) ){
				foreach( $response->fields as $key => $field ){
					
					if( isset( $field->perstag ) ){

						if( in_array( $field->perstag, $cartbounty_custom_fields ) ){ //If we find our custom fields - add their ID values to response array
							$custom_fields_from_activecampaign[] = array(
								'id' 	=> $field->id,
								'tag' 	=> $field->perstag
							);
						}
					}
				}
			}
		}
		return $custom_fields_from_activecampaign;
	}
}