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

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

/**
 * MailPoet Background Sync class
 */
class Mailpoet_Background_Sync {

	/**
	 * If set to true this will echo progress of a sync job.
	 */
	public $debug = false;

	/**
	 * Future proofing multi-connections
	 */
	public $mode = JPCRM_MAILPOET_MODE_LOCAL;

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

	/**
	 * Setup MailPoet Background Sync
	 */
	public function __construct() {

		// load job class
		require_once JPCRM_MAILPOET_ROOT_PATH . 'includes/class-mailpoet-background-sync-job.php';

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

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

	/**
	 * Main Class Instance.
	 *
	 * Ensures only one instance of Mailpoet_Background_Sync is loaded or can be loaded.
	 *
	 * @since 2.0
	 * @static
	 * @see
	 * @return Mailpoet_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 mailpoet() {

		global $zbs;
		return $zbs->modules->mailpoet;
	}

	/**
	 * 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_mailpoet_sync', array( $this, 'cron_job' ) );

		// Syncing based on MailPoet hooks:

		// Subscriber edits/changes:
		add_action( 'mailpoet_subscriber_created', array( $this, 'add_update_subscriber_by_id' ), 1, 1 );
		add_action( 'mailpoet_subscriber_updated', array( $this, 'add_update_subscriber_by_id' ), 1, 1 );
		add_action( 'mailpoet_subscriber_deleted', array( $this, 'delete_subscriber_by_id' ), 1, 1 );
		add_action( 'mailpoet_multiple_subscribers_created', array( $this, 'add_update_subscribers_by_id' ), 1, 1 );
		add_action( 'mailpoet_multiple_subscribers_updated', array( $this, 'add_update_subscribers_by_id' ), 1, 1 );
		add_action( 'mailpoet_multiple_subscribers_deleted', array( $this, 'delete_subscribers_by_id' ), 1, 1 );

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

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

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

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

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

		// fire job
		$this->sync_subscribers();
	}

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

		return defined( 'jpcrm_mailpoet_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_mailpoet_sync'] = '5min';
		}

		return $crons;
	}

	/**
	 * Main job function: this will retrieve and import subscribers from MailPoet
	 *  This can be called in three 'modes'
	 *    - via cron (as defined by `jpcrm_mailpoet_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
	 *
	 * @param bool $silent - if true no output will be returned (for use where we call after `add_update_subscribers_by_id()`)
	 *
	 * @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 subscribers imported is returned
	 */
	public function sync_subscribers( $silent = false ) {

		global $zbs;

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

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

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

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

		}

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

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

		// blocker
		if ( ! defined( 'jpcrm_mailpoet_running' ) ) {

			define( 'jpcrm_mailpoet_running', 1 );

		}

		// init class
		$sync_job = new Mailpoet_Background_Sync_Job( $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'];
			$subscribers_synced     = (int) $sync_result['subscribers_synced'];

		}

		// discern completeness
		// either maxxed pages, or more likely x no = y no
		if ( $total_remaining_pages == 0 || $this->mailpoet()->get_all_mailpoet_subscribers_count() <= $this->mailpoet()->get_crm_mailpoet_contact_count() ) {

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

		} else {

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

		}

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

			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 Subscriber Sync Job: ' . $sync_status );
			$mailpoetsync_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 ),
				'subscribers_synced'               => $subscribers_synced,
				'percentage_completed'             => $overall_percentage,
				'total_crm_contacts_from_mailpoet' => $this->mailpoet()->get_crm_mailpoet_contact_count(),
			);
			$mailpoet_latest_stats     = $this->mailpoet()->get_jpcrm_mailpoet_latest_stats();
			wp_send_json( array_merge( $mailpoet_latest_stats, $mailpoetsync_status_array ), 200, JSON_UNESCAPED_SLASHES );
		}
	}

	/**
	 * Set's a completion status for MailPoet Subscriber imports
	 *
	 * @param string|bool $status = 'yes|no' (#legacy) or 'true|false'
	 *
	 * @return bool $status
	 */
	public function set_first_import_status( $status ) {

		$status_bool = false;

		if ( $status == 'yes' || $status === true ) {

			$status_bool = true;

		}

		// set it
		$this->mailpoet()->settings->update( 'first_import_complete', $status_bool );

		return $status_bool;
	}

	/**
	 * Returns a completion status for MailPoet Subscriber imports
	 *
	 * @return bool $status
	 */
	public function first_import_completed() {

		$status_bool = false;

		// get
		$first_import_complete = $this->mailpoet()->settings->get( 'first_import_complete', false );

		if ( $first_import_complete == 'yes' || $first_import_complete === true || $first_import_complete == 1 ) {

			$status_bool = true;

		}

		return $status_bool;
	}

	/**
	 * Sets current working page index (to resume from)
	 *
	 * @return int $page
	 */
	public function set_resume_from_page( $page_no ) {

		$this->mailpoet()->settings->update( 'resume_from_page', $page_no );

		return $page_no;
	}

	/**
	 * Return current working page index (to resume from)
	 *
	 * @return int $page
	 */
	public function resume_from_page() {

		return $this->mailpoet()->settings->get( 'resume_from_page', 0 );
	}

	/**
	 * Returns 'local' or 'api'
	 *  (whichever mode is selected in settings)
	 */
	public function import_mode( $str_mode = false ) {

		// import mode
		$mode = (int) $this->mode;

		// debug/string mode
		if ( $str_mode ) {
			if ( $mode === 0 ) {
				return 'JPCRM_MAILPOET_MODE_LOCAL';
			} else {
				return 'JPCRM_MAILPOET_MODE_API';
			}
		}

		return $mode;
	}

	/**
	 * Add or Update subscriber
	 * Fired by hooks: mailpoet_subscriber_created, mailpoet_subscriber_updated, mailpoet_subscriber_deleted
	 * Changes caught here:
	 * - first, last names
	 * - email
	 * - doens't seem to fire on: change of newsletter, change of tags
	 */
	public function add_update_subscriber_by_id( int $subscriberId ) {

		global $zbs;

		// should we log changes via contact note?
		$autolog_changes = $this->mailpoet()->settings->get( 'autolog_changes', false );

		// retrieve records
		$potential_subscriber = $this->mailpoet()->get_mailpoet_subscriber_by_subscriber_id( $subscriberId );
		$potential_contact    = $zbs->DAL->contacts->getContact(
			-1,
			array(

				'externalSource'    => 'mailpoet',
				'externalSourceUID' => $subscriberId,

			)
		);

		// got records?
		if (
				is_array( $potential_subscriber ) && isset( $potential_subscriber['email'] )
		) {

			// Update:
			if ( is_array( $potential_contact ) && isset( $potential_contact['id'] ) ) {

				// note changes
				$previous_data   = $potential_contact;
				$contact_changes = array();

				// email (will always be the same until https://github.com/Automattic/zero-bs-crm/issues/2565)
				// ... in fact this next block is defunct as it stands because getSubscriber above gets the subscriber
				// ... AFTER email change.
				if ( $potential_subscriber['email'] != $potential_contact['email'] ) {

					$contact_changes['email'] = sanitize_email( $potential_subscriber->data->email );

					// if email changed, add old as an alias
					// for that we need the old alias list to append to
					$contact_aliases = is_array( $potential_contact['aliases'] ) ? $potential_contact['aliases'] : array();
					if ( ! in_array( $previous_data['email'], $contact_aliases ) ) {

						$contact_aliases[] = $previous_data['email'];

					}
				}

				// first name
				if ( $potential_subscriber['first_name'] != $potential_contact['fname'] ) {

					$contact_changes['fname'] = sanitize_text_field( $potential_subscriber['first_name'] );

				}

				// last name
				if ( $potential_subscriber['last_name'] != $potential_contact['lname'] ) {

					$contact_changes['lname'] = sanitize_text_field( $potential_subscriber['last_name'] );

				}

				// enact changes
				if ( count( $contact_changes ) > 0 ) {

					// we split this into field + contact_aliases changes, because then we can use limitedFields support

					// build limited fields:
					$contact_changes_as_limited_fields = array();
					foreach ( $contact_changes as $key => $value ) {

						$contact_changes_as_limited_fields[] = array(

							'key'  => 'zbsc_' . $key,
							'val'  => $value,
							'type' => '%s', // all are strings here

						);

					}

					// enact
					$zbs->DAL->contacts->addUpdateContact(
						array(

							'id'            => $potential_contact['id'],
							'limitedFields' => $contact_changes_as_limited_fields,

						)
					);

					// any aliases to add?
					if ( isset( $contact_aliases ) && count( $contact_aliases ) ) {

						foreach ( $contact_aliases as $alias ) {

							zeroBS_addObjAlias( ZBS_TYPE_CONTACT, $potential_contact['id'], $alias );
						}
					}

					// do we add logs?
					if ( $autolog_changes == '1' ) {

						// build log
						$object_change_str = '';
						if ( isset( $contact_changes['email'] ) ) {

							$object_change_str .= sprintf( '%s: <code>%s</code> → <code>%s</code><br>', __( 'Email', 'zero-bs-crm' ), $previous_data['email'], $contact_changes['email'] );

						}
						if ( isset( $contact_changes['fname'] ) ) {

							$object_change_str .= sprintf( '%s: <code>%s</code> → <code>%s</code><br>', __( 'First name', 'zero-bs-crm' ), $previous_data['fname'], $contact_changes['fname'] );

						}
						if ( isset( $contact_changes['lname'] ) ) {

							$object_change_str .= sprintf( '%s: <code>%s</code> → <code>%s</code><br>', __( 'Last name', 'zero-bs-crm' ), $previous_data['lname'], $contact_changes['lname'] );

						}

						// add log
						if ( ! empty( $object_change_str ) ) {

							zeroBS_addUpdateLog(
								$potential_contact['id'],
								-1,
								-1,
								array(
									'type'      => __( 'Contact Changed via MailPoet', 'zero-bs-crm' ),
									'shortdesc' => __( 'Contact details changed via connected MailPoet subscriber', 'zero-bs-crm' ),
									'longdesc'  => $object_change_str,
								),
								'zerobs_customer'
							);

						}
					}
				}

				return;

			} else {

				// New addition

				// Note we can't act on this hook because currently the only thing passed is the
				// MailPoet ID, from which the user can't currently (via MailPoet API) be retrieved
				// ... so when they add that we can use $this->mailpoet()->get_mailpoet_subscriber_by_subscriber_id
				// in it's real sense and write logic here to import the addition.
				// For now we hack around this below using their GetSubscribers endpoint with a filter of `minUpdatedAt`
				// ... though that's not a sure bet by any means.
				// see #temporary-workaround
				// gh-2565

			}
		}

		// Temporary workaround for lack of accessibility to getSubscriberByID in MP API
		// in the instance of newly added subs
		// #temporary-workaround
		// Attempts to grab the last inserted sub. This will be hit and miss, but will work smoothly for
		// small, infrequently updated lists
		$last_updated_guess_timestamp = time() + jpcrm_get_wp_timezone_offset_in_seconds() - 1;
		$potential_subscribers        = $this->mailpoet()->get_mailpoet_subscribers( false, false, $last_updated_guess_timestamp, 1, 0, false, true, true );

		if ( is_array( $potential_subscribers ) && count( $potential_subscribers ) > 0 ) {

			// push this sub through our sync import function:

			// init class
			$sync_job = new Mailpoet_Background_Sync_Job( false );
			$sync_job->import_subscriber( $potential_subscribers[0] );

		}
	}

	/**
	 * Delete subscriber
	 * Fired by hooks: mailpoet_subscriber_created, mailpoet_subscriber_updated, mailpoet_subscriber_deleted
	 */
	public function delete_subscriber_by_id( int $subscriberId ) {

		global $zbs;

		// what's the delete action?
		$delete_action = $this->mailpoet()->settings->get( 'delete_action', 'none' );

		// shall we delete the related crm contact?
		if ( $delete_action == 'delete' || $delete_action == 'delete_save_related_objects' ) {

			// retrieve record
			$potential_contact_id = $zbs->DAL->contacts->getContact(
				-1,
				array(

					'externalSource'    => 'mailpoet',
					'externalSourceUID' => $subscriberId,

					'onlyID'            => true,

				)
			);

			// got record?
			if ( $potential_contact_id ) {

				$save_orphans = false;
				if ( $delete_action == 'delete_save_related_objects' ) {

					$save_orphans = true;

				}

				// delete the contact
				$zbs->DAL->contacts->deleteContact(
					array(

						'id'          => $potential_contact_id,
						'saveOrphans' => $save_orphans,

					)
				);

			}
		} elseif ( $delete_action == 'add_note' ) {

			// if it was deleted in MailPoet but user has 'add_note' selected as delete action, we add a log to contact

			// retrieve record
			$potential_contact_id = $zbs->DAL->contacts->getContact(
				-1,
				array(

					'externalSource'    => 'mailpoet',
					'externalSourceUID' => $subscriberId,

					'onlyID'            => true,

				)
			);

			// got record?
			if ( $potential_contact_id ) {

				zeroBS_addUpdateLog(
					$potential_contact_id,
					-1,
					-1,
					array(
						'type'      => __( 'Subscriber deleted in MailPoet', 'zero-bs-crm' ),
						'shortdesc' => __( 'Associated MailPoet subscriber was deleted in MailPoet', 'zero-bs-crm' ),
						'longdesc'  => '',
					),
					'zerobs_customer'
				);

			}
		}

		// if we're not deleting the contact, we need to remove the external source record
		// for the contact, because there's no link any more.
		// ... actually if we leave it in tact it still records useful info (the fact the source was MP)
	}

	/**
	 * Add or Update subscribers
	 * Fired by hooks: mailpoet_multiple_subscribers_created, mailpoet_multiple_subscribers_updated
	 */
	public function add_update_subscribers_by_id( int $minActionTimestamp ) {

		// catch these via sync
		$this->sync_subscribers( true );
	}

	/**
	 * Delete subscribers
	 * Fired by hook: mailpoet_multiple_subscribers_deleted
	 */
	public function delete_subscribers_by_id( array $subscriberIds ) {

		// here we rely on our other function `delete_subscriber_by_id()`
		// which has all of the settings-based delete actions
		if ( count( $subscriberIds ) > 0 ) {

			foreach ( $subscriberIds as $subscriber_id ) {

				$this->delete_subscriber_by_id( $subscriber_id );

			}
		}
	}
}
