<?php
/**
 * Bulk-enqueues all customers and orders for background syncing via Action Scheduler
 *
 * @package WooAttioSync
 */

declare( strict_types=1 );

defined( 'ABSPATH' ) || exit;

/**
 * Provides bulk-sync initiation for customers and orders.
 *
 * Enqueueing is done asynchronously through a self-rescheduling dispatcher action
 * (one batch per run) so a large store never blocks a single admin request, and the
 * concurrency lock is held for the whole dispatch instead of only the first request.
 */
class WooAttioSync_Bulk_Sync {

	/** Customers/orders processed per batch to avoid OOM / request timeouts on large stores */
	private const BATCH_SIZE = 500;

	/** Transient keys used to prevent concurrent bulk syncs */
	public const LOCK_CUSTOMERS = 'wcattio_bulk_customers_lock';
	public const LOCK_ORDERS    = 'wcattio_bulk_orders_lock';

	/** Action Scheduler hooks for the self-rescheduling batch dispatchers */
	public const DISPATCH_CUSTOMERS = 'wcattio_bulk_dispatch_customers';
	public const DISPATCH_ORDERS    = 'wcattio_bulk_dispatch_orders';

	/** How long the lock lives; refreshed on every dispatched batch */
	private const LOCK_TTL = HOUR_IN_SECONDS;

	/**
	 * Register the batch-dispatcher handlers. Called once at plugin boot so
	 * Action Scheduler can invoke them on subsequent requests.
	 */
	public function register(): void {
		add_action( self::DISPATCH_CUSTOMERS, array( $this, 'dispatch_customer_batch' ) );
		add_action( self::DISPATCH_ORDERS, array( $this, 'dispatch_order_batch' ) );
	}

	/**
	 * Returns true while a customer bulk sync is still enqueueing batches
	 */
	public static function is_customer_sync_running(): bool {
		return (bool) get_transient( self::LOCK_CUSTOMERS );
	}

	/**
	 * Returns true while an order bulk sync is still enqueueing batches
	 */
	public static function is_order_sync_running(): bool {
		return (bool) get_transient( self::LOCK_ORDERS );
	}

	/**
	 * Begin a bulk customer sync.
	 *
	 * Only syncs registered users (customers + subscribers). Guest-checkout contacts
	 * have no WordPress user account and must be synced via start_order_bulk_sync().
	 *
	 * @return int Number of customers that will be synced, or -1 when a sync is already running.
	 */
	public function start_bulk_sync(): int {
		if ( ! function_exists( 'as_enqueue_async_action' ) ) {
			return 0;
		}

		if ( get_transient( self::LOCK_CUSTOMERS ) ) {
			return -1;
		}
		set_transient( self::LOCK_CUSTOMERS, 1, self::LOCK_TTL );

		$total = $this->count_customers();
		if ( 0 === $total ) {
			// Nothing to do — finalize immediately so the lock is not left dangling.
			update_option( 'wcattio_last_customer_bulk_sync', gmdate( 'Y-m-d H:i:s' ) );
			delete_transient( self::LOCK_CUSTOMERS );
			return 0;
		}

		as_enqueue_async_action( self::DISPATCH_CUSTOMERS, array( 'offset' => 0 ), WooAttioSync_Sync_Queue::AS_GROUP );

		return $total;
	}

	/**
	 * Begin a bulk order sync, respecting the configured sync statuses.
	 *
	 * This also covers guest-checkout orders whose billing contacts have no WP user account.
	 *
	 * @return int Number of orders that will be synced, or -1 when a sync is already running.
	 */
	public function start_order_bulk_sync(): int {
		if ( ! function_exists( 'as_enqueue_async_action' ) ) {
			return 0;
		}

		if ( get_transient( self::LOCK_ORDERS ) ) {
			return -1;
		}
		set_transient( self::LOCK_ORDERS, 1, self::LOCK_TTL );

		// Respect an intentionally empty status list.
		if ( empty( WooAttioSync_Sync_Queue::synced_statuses() ) ) {
			delete_transient( self::LOCK_ORDERS );
			return 0;
		}

		$total = $this->count_orders();
		if ( 0 === $total ) {
			update_option( 'wcattio_last_order_bulk_sync', gmdate( 'Y-m-d H:i:s' ) );
			delete_transient( self::LOCK_ORDERS );
			return 0;
		}

		as_enqueue_async_action( self::DISPATCH_ORDERS, array( 'offset' => 0 ), WooAttioSync_Sync_Queue::AS_GROUP );

		return $total;
	}

	/**
	 * Dispatcher: enqueue one batch of customer syncs, then reschedule itself for the
	 * next batch or finalize (stamp the last-run time and release the lock).
	 *
	 * @param int $offset Zero-based offset into the customer list.
	 * @return void
	 */
	public function dispatch_customer_batch( int $offset = 0 ): void {
		if ( ! function_exists( 'as_enqueue_async_action' ) ) {
			return;
		}

		$ids = get_users(
			array(
				'role__in' => array( 'customer', 'subscriber' ),
				'fields'   => 'ID',
				'number'   => self::BATCH_SIZE,
				'paged'    => (int) floor( $offset / self::BATCH_SIZE ) + 1,
				'orderby'  => 'ID',
				'order'    => 'ASC',
			)
		);

		foreach ( $ids as $id ) {
			WooAttioSync_Sync_Queue::enqueue_unique( 'wcattio_sync_customer', array( 'customer_id' => (int) $id ) );
		}

		if ( count( $ids ) === self::BATCH_SIZE ) {
			set_transient( self::LOCK_CUSTOMERS, 1, self::LOCK_TTL );
			as_enqueue_async_action( self::DISPATCH_CUSTOMERS, array( 'offset' => $offset + self::BATCH_SIZE ), WooAttioSync_Sync_Queue::AS_GROUP );
			return;
		}

		update_option( 'wcattio_last_customer_bulk_sync', gmdate( 'Y-m-d H:i:s' ) );
		delete_transient( self::LOCK_CUSTOMERS );
	}

	/**
	 * Dispatcher: enqueue one batch of order syncs, then reschedule or finalize.
	 *
	 * @param int $offset Zero-based offset into the order list.
	 * @return void
	 */
	public function dispatch_order_batch( int $offset = 0 ): void {
		if ( ! function_exists( 'as_enqueue_async_action' ) ) {
			return;
		}

		$statuses = WooAttioSync_Sync_Queue::synced_statuses();
		if ( empty( $statuses ) ) {
			delete_transient( self::LOCK_ORDERS );
			return;
		}

		$ids = wc_get_orders(
			array(
				'status'   => $statuses,
				'limit'    => self::BATCH_SIZE,
				'offset'   => $offset,
				'return'   => 'ids',
				'paginate' => false,
				'orderby'  => 'ID',
				'order'    => 'ASC',
			)
		);

		foreach ( $ids as $order_id ) {
			WooAttioSync_Sync_Queue::enqueue_unique( 'wcattio_sync_order', array( 'order_id' => (int) $order_id ) );
		}

		if ( count( $ids ) === self::BATCH_SIZE ) {
			set_transient( self::LOCK_ORDERS, 1, self::LOCK_TTL );
			as_enqueue_async_action( self::DISPATCH_ORDERS, array( 'offset' => $offset + self::BATCH_SIZE ), WooAttioSync_Sync_Queue::AS_GROUP );
			return;
		}

		update_option( 'wcattio_last_order_bulk_sync', gmdate( 'Y-m-d H:i:s' ) );
		delete_transient( self::LOCK_ORDERS );
	}

	/**
	 * Count registered customers/subscribers eligible for sync.
	 *
	 * @return int
	 */
	private function count_customers(): int {
		$counts = count_users();
		$roles  = is_array( $counts ) ? ( $counts['avail_roles'] ?? array() ) : array();

		return (int) ( $roles['customer'] ?? 0 ) + (int) ( $roles['subscriber'] ?? 0 );
	}

	/**
	 * Count orders matching the configured sync statuses.
	 *
	 * @return int
	 */
	private function count_orders(): int {
		$result = wc_get_orders(
			array(
				'status'   => WooAttioSync_Sync_Queue::synced_statuses(),
				'limit'    => 1,
				'return'   => 'ids',
				'paginate' => true,
			)
		);

		return is_object( $result ) ? (int) ( $result->total ?? 0 ) : count( (array) $result );
	}
}
