<?php
/*
 * Jetpack CRM
 * https://jetpackcrm.com
 *
 * MailPoet Sync: Background Sync Job (per run, site connection, currently only 1 local site)
 *
 */
namespace Automattic\JetpackCRM;

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

/**
 * MailPoet Background Sync Job class
 */
class Mailpoet_Background_Sync_Job {

	/**
	 * Paused state
	 */
	private $paused = false;

	/**
	 * Mode (Local/API)
	 */
	private $mode = JPCRM_MAILPOET_MODE_LOCAL;

	/**
	 * Number of subscribers to process per job
	 */
	private $subscribers_per_page = 500;
	private $pages_per_job        = 1;

	/**
	 * Current page the job is working on
	 */
	private $current_page = 1;

	/**
	 * Number of pages in MailPoet
	 */
	private $mailpoet_total_pages = 0;

	/**
	 * Number of subscribers in MailPoet
	 */
	private $mailpoet_total_subscribers = 0;

	/**
	 * A per-job cached list of MailPoet Segment data
	 */
	public $segment_list = false;

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

	/**
	 * Setup MailPoet Background Sync Job
	 */
	public function __construct( $debug = false, $subscribers_per_page = 50, $pages_per_job = 1 ) {

		// set vars
		$this->debug                = $debug;
		$this->subscribers_per_page = $subscribers_per_page;
		$this->pages_per_job        = $pages_per_job;

		// promote paused state
		// <for now we're pausing on this pause functionality>
	}

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

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

	/**
	 * Returns full settings array from main settings class
	 */
	public function settings() {

		return $this->mailpoet()->settings->getAll();
	}

	/**
	 * 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>';

		}
	}

	/**
	 * Main job function: this will retrieve and import subscribers from MailPoet into CRM.
	 *
	 * @return mixed (int|json)
	 *   - if cron originated: a count of subscribers 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 run_sync() {

		global $zbs;

		$this->debug( 'Fired `run_sync()`' );

		// prep vars
		$run_sync_job          = true;
		$total_remaining_pages = 0;
		$total_pages           = 0;
		$errors                = array();
		$subscribers_synced    = 0;

		// check not marked 'paused'
		if ( $this->paused ) {

			// skip it
			$this->debug( 'Skipping Sync (mode: ' . $this->mode . ') - Paused' );
			$run_sync_job = false;

		}

		$this->debug( 'Starting Sync (mode: ' . $this->mode . ')' );

		// switch by mode
		if ( $this->mode == JPCRM_MAILPOET_MODE_LOCAL ) {

			// local install

			// verify mailpoet installed
			if ( ! $zbs->mailpoet_is_active() ) {

				$status_short_text = __( 'Missing MailPoet', 'zero-bs-crm' );

				$this->debug( $status_short_text );

				$errors[] = array(
					'status'            => 'error',
					'status_short_text' => $status_short_text,
					'status_long_text'  => __( 'MailPoet Sync will start importing data when you have installed the MailPoet plugin.', 'zero-bs-crm' ),
					'error'             => 'local_no_mailpoet',
				);

				// skip this site connection
				$run_sync_job = false;

			}
		} else {

			// no mode, or a faulty one!
			$this->debug( 'Mode unacceptable' );

			$errors[] = array(
				'status'            => 'error',
				'status_short_text' => $status_short_text,
				'status_long_text'  => __( 'MailPoet Sync could not sync because it is in an unacceptable mode.', 'zero-bs-crm' ),
				'error'             => 'mode_error',
			);

			// skip this site connection
			$run_sync_job = false;

		}

		if ( $run_sync_job ) {

			$this->debug( 'Running Import of ' . $this->pages_per_job . ' pages' );

			// retrieve segment (list) summary data (used to tag contacts with their list participation if option)
			$this->segment_list = $this->mailpoet()->get_mailpoet_lists_summary( true );

			// do x pages
			for ( $i = 0; $i < $this->pages_per_job; $i++ ) {

				// Get last working position. The page range is 0..(total_pages - 1)
				$page_to_retrieve = $this->resume_from_page();

				// ... if for some reason we've got a negative, start from scratch.
				if ( $page_to_retrieve < 0 ) {
					$page_to_retrieve = 0;
				}

				$this->current_page = $page_to_retrieve;

				// import the page of subscribers
				// This always returns the count of imported subscribers,
				// unless 100% sync is reached, at which point it will exit (if called via AJAX)
				// for now, we don't need to track the return
				$subscribers_synced += (int) $this->import_page_of_subscribers( $page_to_retrieve );

				$this->debug( 'Subscribers completed: ' . min( ( $this->current_page * $this->subscribers_per_page ) + $subscribers_synced, $this->mailpoet_total_subscribers ) . ' / ' . $this->mailpoet_total_subscribers );
			}

			// mark the pass
			$this->mailpoet()->settings->update( 'last_sync_fired', time() );
			$this->debug( 'Sync Job finished with percentage complete: ' . $this->percentage_completed( false ) . '% complete.' );

		}

		// return overall % counts later used to provide a summary % across sync site connections
		$percentage_counts = $this->percentage_completed( true );
		if ( is_array( $percentage_counts ) ) {
			$total_pages           = (int) $percentage_counts['total_pages'];
			$total_remaining_pages = $percentage_counts['total_pages'] - ( $percentage_counts['page_no'] + 1 );
		}

		// We should never have less than zero here
		// (seems to happen when site connections error out)
		if ( $total_remaining_pages < 0 ) {
			$total_remaining_pages = 0;
		}

		return array(
			'total_pages'           => $total_pages,
			'total_remaining_pages' => $total_remaining_pages,
			'errors'                => $errors,
			'subscribers_synced'    => $subscribers_synced,
		);
	}

	/**
	 * Retrieve and process 1 page of MailPoet Subscribers from local install (or later, API)
	 *
	 * @param int $page_no - the page number to start from. Position: 0..(total_pages - 1).
	 *
	 * @return mixed (int|json)
	 *   - if cron originated: a count of subscribers 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
	 */
	private function import_page_of_subscribers( $page_no ) {

		$this->debug( 'Fired `import_page_of_subscribers( ' . $page_no . ' )`, importing from ' . $this->import_mode( true ) . '.' );

		// store/api switch
		if ( $this->import_mode() === JPCRM_MAILPOET_MODE_LOCAL ) {

			// Local
			return $this->import_local_subscribers( $page_no );

		} else {

			// not yet implemented, external import (JPCRM_MAILPOET_MODE_API)
			return false;

		}
	}

	/**
	 * Retrieve and process a page of MailPoet Subscribers from local install
	 *
	 * @param int $page_no
	 *
	 * @return mixed (int|json)
	 *   - if cron originated: a count of subscribers 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 import_local_subscribers( $page_no = -1 ) {

		// Where we're trying to run without MailPoet, fail.
		// In theory we shouldn't ever hit this, as we catch it earlier.
		global $zbs;
		if ( ! $zbs->mailpoet_is_active() ) {
			$this->debug( 'Unable to import as it appears the MailPoet plugin is not installed.' );
			return false;
		}

		// catch paging
		if ( $page_no < 0 ) {
			$page_no = 0;
		}
		$limit  = $this->subscribers_per_page;
		$offset = $this->subscribers_per_page * $page_no;

		$this->debug( 'Retrieving page ' . $page_no . ' of subscribers (limit=' . $limit . ', offset=' . $offset . ')' );

		$subscribers_synced = 0;

		// Later: Allow per-list syncing, (for now this grabs all)
		$mailpoet_subscribers = $this->mailpoet()->get_all_mailpoet_subscribers( $limit, $offset, false, true, true );

		// got subs?
		if ( is_array( $mailpoet_subscribers ) ) {

			// cycle through and import
			foreach ( $mailpoet_subscribers as $subscriber ) {

				// will be an assoc arr of sub details
				$this->import_subscriber( $subscriber );

				++$subscribers_synced;

			}
		} else {

			$this->debug( 'No MailPoet subscribers found (have we reached the end of the list?)' );
			return false;

		}

		// check for completion
		$total_page_count = $this->get_total_page_count();
		if ( $page_no >= ( $total_page_count - 1 ) ) {

			$this->debug( 'MailPoet subscriber import complete!' );

			// we're at 100%, mark sync complete
			$this->set_first_import_status( true );

			// set pointer to last processed page
			$this->set_resume_from_page( $page_no );

			// return count
			return $subscribers_synced;

		}

		// There's still pages to go then:

		// increase pointer by one, (only if we've got a full pages worth)
		if ( $subscribers_synced === $this->subscribers_per_page ) {
			$this->set_resume_from_page( $page_no + 1 );
		}

		// return the count
		return $subscribers_synced;
	}

	/**
	 * Takes a MailPoet associative array for a subscriber and adds/updates a crm contact
	 *
	 * @param array $subscriber
	 *
	 * @return int $contact_id
	 */
	public function import_subscriber( $subscriber ) {

		global $zbs;

		// get settings
		$settings = $this->mailpoet()->settings->getAll();

		// get wpid where passed
		$wpid = -1;
		if ( isset( $subscriber['wp_user_id'] ) && ! empty( $subscriber['wp_user_id'] ) ) {

			$wpid = $subscriber['wp_user_id'];

		}

		// unused subscriber attributes:
		// is_woocommerce_user, status, subscribed_ip, confirmed_ip, confirmed_at, last_subscribed_at,
		// updated_at, deleted_at, unconfirmed_data, source (e.g. WordPress user), count_confirmations,
		// unsubscribe_token, link_token, engagement_score, engagement_score_updated_at, last_engagement_at
		// woocommerce_synced_at, email_count
		$contact_args = array(

			'data' => array(

				'email'           => sanitize_email( $subscriber['email'] ),
				'fname'           => sanitize_text_field( $subscriber['first_name'] ),
				'lname'           => sanitize_text_field( $subscriber['last_name'] ),
				'wpid'            => $wpid,

				'externalSources' => array(
					array(
						'source' => 'mailpoet',
						'uid'    => $subscriber['id'],
						'origin' => '', // for now this is always same-site
						'owner'  => 0, // for now we hard-type no owner to avoid ownership issues. As we roll out fuller ownership we may want to adapt this.
					),
				),

				'created'         => strtotime( $subscriber['created_at'] ),

			),
			// 'extraMeta' => array()

		);

		// tags
		$tags = array();
		if ( $settings['tag_with_list'] == 1 ) { // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual

			// tag with subscriber list
			if ( isset( $subscriber['subscriptions'] ) && is_array( $subscriber['subscriptions'] ) ) {

				foreach ( $subscriber['subscriptions'] as $subscription ) {

					// only for subs
					if ( $subscription['status'] === 'subscribed' ) {

						/* // phpcs:ignore Squiz.PHP.CommentedOutCode.Found
								[id] => 1
								[subscriber_id] => 1
								[created_at] => 2022-10-11 07:55:51
								[segment_id] => 1
								[status] => subscribed
								[updated_at] => 2022-10-11 07:55:51
						*/

						// find segment (list) name from our summary data
						// note: if this doesn't find a match it's likely
						// id 1+2 which seem to be WP and Woo user data sets
						// which are not returned by the API `getLists()`
						$segment_name = ( isset( $this->segment_list[ $subscription['segment_id'] ] ) ? $this->segment_list[ $subscription['segment_id'] ]['name'] : '' );

						// valid list name?
						// here we sidestep any with suffix `| CRM` or `| Jetpack CRM`
						// .. to avoid us exporting a CRM segment into a list, then reimporting and adding a tag we created
						if ( ! empty( $segment_name ) && ! str_ends_with( $segment_name, '| CRM' ) && ! str_ends_with( $segment_name, '| Jetpack CRM' ) ) {

							$tags[] = sanitize_text_field( $settings['tag_list_prefix'] . $segment_name );

						}
					}
				}
			}
		}

		if ( $settings['tag_with_tags'] == 1 ) { // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual

			// tag with subscriber tags
			if ( isset( $subscriber['tags'] ) && is_array( $subscriber['tags'] ) ) {

				foreach ( $subscriber['tags'] as $tag ) {

					/* // phpcs:ignore Squiz.PHP.CommentedOutCode.Found
						[id] => 54
						[subscriber_id] => 1
						[tag_id] => 4
						[created_at] => 2022-11-03 13:23:38
						[updated_at] => 2022-11-03 13:23:38
						[name] => xxx
					*/

					// here we sidestep any with suffix `| CRM` or `| Jetpack CRM`
					// .. to avoid us exporting a CRM segment into a list, then reimporting and adding a tag we created
					if ( ! str_ends_with( $tag['name'], '| CRM' ) && ! str_ends_with( $tag['name'], '| Jetpack CRM' ) ) {

						$tags[] = sanitize_text_field( $settings['tag_tag_prefix'] . $tag['name'] );

					}
				}
			}
		}

		// got tags?
		if ( count( $tags ) > 0 ) {

			$contact_args['data']['tags']     = $tags;
			$contact_args['data']['tag_mode'] = 'append';

		}

		// $this->debug( 'Add/Update contact: <pre>' . var_export( $contact_args ) . '</pre>' );

		// Add/update the contact & return id
		return $zbs->DAL->contacts->addUpdateContact( // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
			array(
				'data'                 => $contact_args['data'],
				'do_not_update_blanks' => true,
			)
		);
	}

	/**
	 * Returns a number for total pages to process (with current $this->subscribers_per_page)
	 *
	 * @return int $total_pages
	 */
	public function get_total_page_count() {

		// calculate it
		$this->mailpoet_total_subscribers = $this->mailpoet()->get_all_mailpoet_subscribers_count();
		$total_pages                      = 0;
		if ( $this->subscribers_per_page > 0 && $this->mailpoet_total_subscribers > 0 ) {

			$total_pages = ceil( $this->mailpoet_total_subscribers / $this->subscribers_per_page );

		}

		$this->mailpoet_total_pages = $total_pages;

		return $total_pages;
	}

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

		return $this->mailpoet()->background_sync->set_first_import_status( $status );
	}

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

		return $this->mailpoet()->background_sync->first_import_completed();
	}

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

		return $this->mailpoet()->background_sync->set_resume_from_page( $page_no );
	}

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

		return $this->mailpoet()->background_sync->resume_from_page();
	}

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

		return $this->mailpoet()->background_sync->import_mode( $str_mode );
	}

	/**
	 * Attempts to return the percentage completed of a sync
	 *
	 * @param bool $return_counts - Return counts (if true returns an array inc % completed, x of y pages)
	 * @param bool $use_cache - use values cached in object instead of retrieving them directly from MailPoet
	 *
	 * @return int|bool - percentage completed, or false if not attainable
	 */
	public function percentage_completed( $return_counts = false, $use_cache = true ) {

		// if not using cache, retrieve values from MailPoet
		if ( ! $use_cache ) {

			// could probably abstract the retrieval of subscribers for more nesting. For now it's fairly DRY as only in 2 places.

				// Local store
				$this->mailpoet_total_pages = $this->get_total_page_count();

		}

		// calculate completeness
		if ( $this->mailpoet_total_pages === 0 ) {

			// no subscribers to sync, so complete
			$percentage_completed = 100;

		} else {
			$percentage_completed = 100 * ( $this->current_page + 1 ) / $this->mailpoet_total_pages;
		}

		$this->debug( 'Percentage completed: ' . $percentage_completed . '%' );
		$this->debug( 'Pages completed: ' . ( $this->current_page + 1 ) . ' / ' . $this->mailpoet_total_pages );

		if ( $return_counts ) {

			return array(
				'page_no'              => $this->current_page,
				'total_pages'          => $this->mailpoet_total_pages,
				'percentage_completed' => $percentage_completed,
			);

		}

		// return
		if ( $percentage_completed >= 0 ) {

			return $percentage_completed;

		}

		return false;
	}
}
