<?php 
/*!
 * Jetpack CRM
 * https://jetpackcrm.com
 *
 * WooSync: Background Sync
 *
 */
namespace Automattic\JetpackCRM;

// block direct access
defined( 'ZEROBSCRM_PATH' ) || exit( 0 );

/**
 * WooSync Background Sync class
 */
class Woo_Sync_Background_Sync {

	/**
	 * Ready Mode, 
	 * No syncing will run unless this is set to true. 
	 * This is designed to allow for migrations to run on update, before collision-likely sync runs.
	 */
	private $ready_mode = false;
	
	/**
	 * If set to true this will echo progress of a sync job.
	 */
	public $debug = false;

	/*
	 * Tax rates table storage
	 */
	private $tax_rates_table = false;

	/**
	 * The single instance of the class.
	 */
	protected static $_instance = null;

	/**
	 * Setup WooSync Background Sync
	 * Note: This will effectively fire after core settings and modules loaded
	 * ... effectively on tail end of `init`
	 */
	public function __construct() {

		// check we're good to go
		$this->verify_ready_mode();

		if ( $this->ready_mode ){

			// load job class
			require_once JPCRM_WOO_SYNC_ROOT_PATH. 'includes/class-woo-sync-background-sync-job.php';

			// Initialise Hooks
			$this->init_hooks();

			// Schedule cron
			$this->schedule_cron();

		}

	}
		

	/**
	 * Main Class Instance.
	 *
	 * Ensures only one instance of Woo_Sync_Background_Sync is loaded or can be loaded.
	 *
	 * @since 2.0
	 * @static
	 * @see 
	 * @return Woo_Sync_Background_Sync main instance
	 */
	public static function instance() {
		if ( is_null( self::$_instance ) ) {
			self::$_instance = new self();
		}
		return self::$_instance;
	}


	/**
	 * Returns main class instance
	 */
	public function woosync(){

		global $zbs;
		return $zbs->modules->woosync;

	}


	/**
	 * Checks that all 'pre-ready' conditions are met before enabling sync
	 * (Critical migrations when we moved from 1 to many site syncing)
	 */
	public function verify_ready_mode() {

		// check for critical migration when we moved from 1 to many site syncing
		$migration_status = get_option( 'jpcrm_woosync_52_mig' );		
		if ( $migration_status ){

			$this->ready_mode = true;

		}

		return $this->ready_mode;

	}

	
	/**
	 * If $this->debug is true, outputs passed string
	 *
	 * @param string - Debug string
	 */
	private function debug( $str ){

		if ( $this->debug ){

			echo '[' . zeroBSCRM_locale_utsToDatetime( time() ) . '] ' . $str . '<br>';

		}

	}


	/**
	 * Initialise Hooks
	 */
	private function init_hooks() {

		// cron
		add_action( 'jpcrm_woosync_sync', array( $this, 'cron_job' ) );

		// add our cron task to the core crm cron monitor list
		add_filter( 'jpcrm_cron_to_monitor', array( $this, 'add_cron_monitor' ) );

		global $zbs;

		// Abort if Woo isn't active.
		if ( ! $zbs->woocommerce_is_active() ) {
			return;
		}

		// Order changes:
		add_action( 'woocommerce_order_status_changed', array( $this, 'add_update_from_woo_order' ), 1, 1 );
		add_action( 'woocommerce_process_shop_order_meta', array( $this, 'add_update_from_woo_order' ), 100, 1 );
		add_action( 'woocommerce_deposits_create_order', array( $this, 'add_update_from_woo_order' ), 100, 1 );
		if ( jpcrm_woosync_is_hpos_enabled() ) {
			// These hooks are available as of Woo 7.1.0 and are required for HPOS.
			add_action( 'woocommerce_before_trash_order', array( $this, 'woocommerce_order_trashed' ), 10, 1 );
			add_action( 'woocommerce_before_delete_order', array( $this, 'woocommerce_order_deleted' ), 10, 1 );
		} else {
			add_action( 'wp_trash_post', array( $this, 'woocommerce_order_trashed' ), 10, 1 );
			add_action( 'before_delete_post', array( $this, 'woocommerce_order_deleted' ), 10, 1 );
		}

		// Catch WooCommerce customer address changes and update contact:
		add_action( 'woocommerce_customer_save_address', array( $this, 'update_contact_address_from_wp_user' ), 10, 3 );
	}

	/**
	 * Setup cron schedule
	 */
	private function schedule_cron() {

		// schedule it
		if ( ! wp_next_scheduled( 'jpcrm_woosync_sync' ) ) {
		  wp_schedule_event( time(), '5min', 'jpcrm_woosync_sync' );
		}	

	}


	/**
	 * Run cron job
	 */
	public function cron_job(){

		// define global to mark this as a cron call
		define( 'jpcrm_woosync_cron_running', 1 );

		// fire job
		$this->sync_orders();

	}

	/**
	 * Returns bool as to whether or not the current call was made via cron
	 */
	private function is_cron(){

		return defined( 'jpcrm_woosync_cron_running' );

	}


	/**
	 * Filter call to add the cron zbssendbot to the watcher system
	 *
	 * @param array $crons
	 * @return array
	 */
	function add_cron_monitor( $crons ) {

		if ( is_array( $crons ) ) {

			$crons[ 'jpcrm_woosync_sync' ] = '5min'; //'hourly';
		}

		return $crons;
	}


	/**
	 * Main job function: using established settings, this will retrieve and import orders
	 *  from WooCommerce into CRM. This can be called in three 'modes'
	 *    - via cron (as defined by `jpcrm_woosync_cron_running`)
	 *    - via AJAX (if not via cron and not in debug mode)
	 *    - for debug (if $this->debug is set) This is designed to be called inline and will output progress of sync job
	 *
	 *
	 * @return mixed (int|json)
	 *   - if cron originated: a count of orers imported is returned
	 *   - if not cron originated (assumes AJAX):
	 *      - if completed sync: JSON summary info is output and then exit() is called
	 *      - else count of orders imported is returned
	 */
	public function sync_orders(){

		global $zbs;

		$this->debug( 'Fired `sync_orders()`.' );

		if ( !$this->ready_mode ){

			$this->debug( 'Blocked by !ready_mode.' );

			// return blocker error
			return array( 
				'status' => 'not_in_ready_mode',
				'status_short_text' => __( 'Unable to complete migration 5.2', 'zero-bs-crm' ),
				'status_long_text' => __( 'WooSync was unable to complete a necessary migration and therefore cannot yet sync.', 'zero-bs-crm' ),
			);

		}

		$sync_sites = $this->woosync()->get_active_sync_sites();

		$this->debug( 'Sync Sites:<pre>' . print_r( $sync_sites, 1 ) . '</pre>' );

		if ( !is_array( $sync_sites ) ){
			
			$this->debug( 'Failed to retrieve sites to sync! ' );

		}

		// check not currently running
		if ( defined( 'jpcrm_woosync_running' ) ) {

			$this->debug( 'Attempted to run `sync_orders()` when job already in progress.' );

			// return blocker error
			return array( 'status' => 'job_in_progress' );

		}

		$this->debug( 'Commencing syncing ' . count( $sync_sites ) . ' sites.' );

		// prep silos
		$total_remaining_pages = 0;
		$total_pages = 0;
		$total_active_connections = 0;
		$total_paused_connections = 0;
		$errors = array();

		// cycle through each sync site and attempt sync
		foreach ( $sync_sites as $site_key => $site_info ){

			// check not marked 'paused'
			if ( isset( $site_info['paused'] ) && $site_info['paused'] ){

				// skip it
				$total_paused_connections++;
				$this->debug( 'Skipping Sync for ' . $site_info['domain'] . ' (mode: ' . $site_info['mode'] . ') - Paused' );
				continue;

			}

			$this->debug( 'Starting Sync for ' . $site_info['domain'] . ' (mode: ' . $site_info['mode'] . ')' );
			$total_active_connections++;

			// blocker			
			if ( !defined( 'jpcrm_woosync_running' ) ) {
			
				define( 'jpcrm_woosync_running', 1 );
			
			}

			// init class
			$sync_job = new Woo_Sync_Background_Sync_Job( $site_key, $site_info, $this->debug );

			// start sync job
			$sync_result = $sync_job->run_sync();
			
			$this->debug( 'Sync Result:<pre>' . print_r( $sync_result, 1 ) . '</pre>' );

			/* will be
			false

			or

			array(

				'total_pages'           => $total_pages,
				'total_remaining_pages' => $total_remaining_pages,
				'errors'                => $errors,

			);*/

			if ( is_array( $sync_result ) && isset( $sync_result['total_pages'] ) && isset( $sync_result['total_remaining_pages'] ) ){

				// maintain overall % counts later used to provide a summary % across sync site connections
				$total_pages += (int)$sync_result['total_pages'];
				$total_remaining_pages += $sync_result['total_remaining_pages'];

			}

		}

		// discern completeness
		if ( $total_active_connections > 0 ){

			if ( $total_paused_connections === 0 ){

				// no paused connections

				if ( $total_remaining_pages == 0 ){

					$sync_status = 'sync_completed';
					$overall_percentage = 100;
					$status_short_text = __( 'Sync Completed', 'zero-bs-crm' );
					$status_long_text = __( 'WooSync has imported all existing orders and will continue to import future orders.', 'zero-bs-crm' );

				} else {

					$sync_status = 'sync_part_complete';
					$overall_percentage = (int)( ( $total_pages - $total_remaining_pages ) / $total_pages * 100 );
					$status_short_text = __( 'Syncing content from WooCommerce...', 'zero-bs-crm' );
					$status_long_text = '';

				}

			} else {

				// has some paused connections

				if ( $total_remaining_pages == 0 ){

					$sync_status = 'sync_completed';
					$overall_percentage = 100;
					$status_short_text = __( 'Sync Completed for active connections', 'zero-bs-crm' );
					$status_long_text = __( 'WooSync has imported existing orders for sites with active connections, but could not import from paused site connections.', 'zero-bs-crm' );

				} else {

					$sync_status = 'sync_part_complete';
					$overall_percentage = (int)( ( $total_pages - $total_remaining_pages ) / $total_pages * 100 );
					$status_short_text = __( 'Syncing content from WooCommerce...', 'zero-bs-crm' );
					$status_long_text = '';

				}

			}

		} else {

			if ( $total_remaining_pages == 0 ){

				$sync_status = 'sync_completed';
				$overall_percentage = 100;
				$status_short_text = __( 'Sync Previously Completed', 'zero-bs-crm' );
				$status_long_text = __( 'WooSync imported orders previously, but is not currently actively syncing due to paused connections.', 'zero-bs-crm' );

			} else {

				$sync_status = 'sync_part_complete';
				$overall_percentage = (int)( ( $total_pages - $total_remaining_pages ) / $total_pages * 100 );
				$status_short_text = __( 'WooSync is trying to sync, but cannot retrieve all orders due to paused connections.', 'zero-bs-crm' );
				$status_long_text = '';

			}


		}

		// if cron, we just return count
		if ( $this->is_cron() ) {

			return array(

					'status'               => $sync_status, // sync_completed sync_part_complete job_in_progress error
					'status_short_text'    => $status_short_text,
					'percentage_completed' => $overall_percentage,

			);

		} else {

			$this->debug( 'Completed multi-site sync job: ' . $sync_status );
			$woosync_status_array = array(
				'status'               => $sync_status,
				'status_short_text'    => $status_short_text,
				'status_long_text'     => $status_long_text,
				'page_no'              => ( $total_pages - $total_remaining_pages ),
				'orders_imported'      => 0,
				'percentage_completed' => $overall_percentage,
			);
			$woosync_latest_stats = $this->woosync()->get_jpcrm_woo_latest_stats();
			wp_send_json( array_merge( $woosync_latest_stats, $woosync_status_array ) );

		}

	}



	/**
	 * Update contact address of a wp user (likely WooCommerce user)
	 *
	 * @param int $user_id (WordPress user id)
	 * @param string $address_type (e.g. `billing`)
	 */
	public function update_contact_address_from_wp_user( $user_id = -1, $address_type = 'billing' ){

		global $zbs;

		// retrieve contact ID from WP user ID
		$contact_id 	= $zbs->DAL->contacts->getContact(array(
			'WPID'      => $user_id,
			'onlyID'    => true
		));

		if ( $contact_id > 0 ){

			// retrieve customer data from WP user ID
			$woo_customer_meta 	= get_user_meta( $user_id );
			
			if ( $address_type == 'billing' ){

				$data = array(
						'addr1' 	=> $woo_customer_meta['billing_address_1'][0],
						'addr2' 	=> $woo_customer_meta['billing_address_2'][0],
						'city' 		=> $woo_customer_meta['billing_city'][0],
						'county' 	=> $woo_customer_meta['billing_state'][0],
						'country' 	=> $woo_customer_meta['billing_country'][0],
						'postcode' 	=> $woo_customer_meta['billing_postcode'][0],
				);

			} else {
			
				$data = array(
						'secaddr1' 		=> $woo_customer_meta['shipping_address_1'][0],
						'secaddr2' 		=> $woo_customer_meta['shipping_address_2'][0],
						'seccity' 		=> $woo_customer_meta['shipping_city'][0],
						'seccounty' 	=> $woo_customer_meta['shipping_state'][0],
						'seccountry' 	=> $woo_customer_meta['shipping_country'][0],
						'secpostcode' 	=> $woo_customer_meta['shipping_postcode'][0],
				);
			
			}

			// addUpdate as limited fields
			$limited_fields_array = array();
			foreach ( $data as $k => $v ){

				$limited_fields_array[] = array(

					'key' => 'zbsc_' .$k,
					'val' => $v,
					'type'=> '%s'

				);

			}

			// then addUpdate
			$zbs->DAL->contacts->addUpdateContact(array(

				'id'             => $contact_id,
				'limitedFields'  => $limited_fields_array

			));

		}

	}


	/**
	 * Catches trashing of WooCommerce orders and (optionally) removes transactions from CRM
	 *
	 * @param int $order_post_id
	 */
	public function woocommerce_order_trashed( $order_post_id ) {

		// retrieve action
		$delete_action = $this->woosync()->settings->get( 'auto_trash', false );

		// action?
		if ( $delete_action === 'do_nothing' ) {
			return;
		}

		// act
		$this->woocommerce_order_removed( $order_post_id, $delete_action );

	}


	/**
	 * Catches deletion of WooCommerce orders and (optionally) removes transactions from CRM
	 *
	 * @param int $order_post_id
	 */
	public function woocommerce_order_deleted( $order_post_id ) {

		// retrieve action
		$delete_action = $this->woosync()->settings->get( 'auto_delete', false );

		// action?
		if ( $delete_action === 'do_nothing' ) {
			return;
		}

		// act
		$this->woocommerce_order_removed( $order_post_id, $delete_action );

	}


	/**
	 * Catches deletion of WooCommerce orders and (optionally) removes transactions from CRM
	 *
	 * @param int    $order_post_id Order post ID.
	 * @param string $delete_action Delete action.
	 */
	private function woocommerce_order_removed( $order_post_id, $delete_action ) {

		global $zbs;

		if ( ! jpcrm_woosync_is_hpos_enabled() ) {
			// was it an order that was deleted?
			$post_type = get_post_type( $order_post_id );
			if ( $post_type !== 'shop_order' ) {
				return;
			}
		}

		// catch default
		if ( empty( $delete_action ) ) {
			$delete_action = 'change_status';
		}

		// retrieve order
		$order = wc_get_order( $order_post_id );

		// Sometimes there's a custom order number...
		if ( method_exists( $order, 'get_order_number' ) ) {
			$order_num = $order->get_order_number();
		} else {
			// order number by default is the same as the order post ID
			$order_num = $order_post_id;
		}

		// get transaction
		$transaction_id = $this->woosync()->get_transaction_from_order_num( $order_num, '', true );

		if ( $transaction_id > 0 ) {

			// retrieve any associated invoices
			$invoice_id = $zbs->DAL->transactions->get_transaction_invoice_id( $transaction_id );

			// act
			switch ( $delete_action ) {

				// change the transaction (and invoice) status to 'Deleted'
				case 'change_status':

					// set status
					$zbs->DAL->transactions->setTransactionStatus( $transaction_id, __( 'Deleted', 'zero-bs-crm' ) );

					// Also change the status on any woo-created associated invoice
					if ( $invoice_id > 0 ) {
						$zbs->DAL->invoices->setInvoiceStatus( $invoice_id, __( 'Deleted', 'zero-bs-crm' ) );
					}

					break;

				// Delete the transaction (and invoice) and add log to contact
				case 'hard_delete_and_log':

					// delete transaction
					$zbs->DAL->transactions->deleteTransaction( array(
						'id' => $transaction_id
					));

					// Also delete any woo-created associated invoice
					if ( $invoice_id > 0 ) {
						$zbs->DAL->invoices->deleteInvoice( array(
							'id'            => $invoice_id,
							'saveOrphans'   => false
						));
					}

					// get contact(s) to add log to
					// only 1:1 via ui currently, but is support for many in DAL
					$contacts = $zbs->DAL->transactions->get_transaction_contacts( $transaction_id );

					if ( is_array( $contacts ) ) {

						foreach ( $contacts as $contact ) {

							// add log
							$zbs->DAL->logs->addUpdateLog( array(

								'data' => array(

									'objtype'   => ZBS_TYPE_CONTACT,
									'objid'     => $contact['id'],
									'type'      => 'transaction_deleted',
									'shortdesc' => __( 'WooCommerce Order Deleted', 'zero-bs-crm' ),
									'longdesc'  => sprintf( __( 'Transaction #%s was removed from your CRM after the related WooCommerce order #%s was deleted.', 'zero-bs-crm' ), $transaction_id, $order_num )

								),

							));

						}

					}

					break;

			}

		}

	}

	/**
	 * Add or update an order from local WooCommerce (passthru to sync job)
	 *
	 * @param int $order_id - Order id from WooCommerce (may be different than $order_num)
	 */
	public function add_update_from_woo_order( $order_id ) {

		$sync_sites = $this->woosync()->settings->get( 'sync_sites' );

		// if there's a local site and if it's not paused
		if ( isset( $sync_sites['local'] ) && empty( $sync_sites['local']['paused'] ) ) {

			$local_site_info = $sync_sites['local'];

			$local_sync_job = new Woo_Sync_Background_Sync_Job( 'local', $local_site_info, $this->debug );

			$local_sync_job->add_update_from_woo_order( $order_id );

		}

	}

	/**
	 * Get tax rates table (cached)
	 */
	public function get_tax_rates_table( $refresh_from_db = false ) {

		if ( !is_array( $this->tax_rates_table ) || $refresh_from_db ){

			// retrieve tax table to feed in tax links
			$this->tax_rates_table = zeroBSCRM_taxRates_getTaxTableArr( true );

		}

		return $this->tax_rates_table;

	}
}
