<?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 ( self::$_instance === null ) {
			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   = '';

				}
			}
		} elseif ( $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 ), 200, JSON_UNESCAPED_SLASHES );

		}
	}

	/**
	 * 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 #%1$s was removed from your CRM after the related WooCommerce order #%2$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;
	}
}
