<?php
/**
 * Dashboard Handler
 *
 * Handles business logic for dashboard operations such as channel management,
 * order retrieval and sync-queue introspection. Keeping heavy logic outside of
 * controllers allows controllers to focus purely on validation & orchestration
 * responsibilities.
 *
 * @package    MOMCS_Sync
 * @subpackage MOMCS_Sync\includes\common\handlers
 * @author     miniOrange
 */

declare(strict_types=1);

namespace MOMCS_Sync\Includes\Common\Handlers;

use MOMCS_Sync\Includes\Common\Utils\Logger;
use MOMCS_Sync\Includes\Common\Utils\Marketplace_IDs;
use MOMCS_Sync\Includes\Common\Constants\Plugin_Constants;
use MOMCS_Sync\Includes\Common\Models\Multichannels_Model;
use MOMCS_Sync\Includes\Common\Models\Product_Mapping_Model;
use MOMCS_Sync\Includes\Common\Models\Authentication_Details_Model;

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

/**
 * Class Dashboard_Handler
 *
 * This class centralises the heavy-lifting previously done inside
 * Dashboard_Controller.  Controller methods now only validate inputs,
 * check capabilities/nonces and delegate to this handler.
 */
class Dashboard_Handler {

	/**
	 * Logger instance.
	 *
	 * @var Logger
	 */
	private Logger $logger;

	/**
	 * Constructor.
	 */
	public function __construct() {
		$this->logger = new Logger( 'dashboard-handler' );
	}

	/**
	 * Get Action Scheduler statuses that should appear in audit listings.
	 *
	 * Sync audits are historical by design, so the queries must include terminal
	 * states in addition to currently pending/running actions.
	 *
	 * @return array<int, string>
	 */
	private function get_audit_action_statuses(): array {
		return array( 'pending', 'in-progress', 'complete', 'failed', 'canceled' );
	}


	/**
	 * Delete a channel and its related product mappings.
	 *
	 * @param int $channel_id Channel primary-key ID in multichannels table.
	 *
	 * @throws \RuntimeException If the channel is not found.
	 *
	 * @return void
	 */
	public function delete_channel( int $channel_id ): void {
		$multichannels_model = new Multichannels_Model();
		$channel             = $multichannels_model->get_channel_by_id( $channel_id );

		if ( ! $channel ) {
			throw new \RuntimeException( esc_html__( 'Channel not found', 'multi-channel-for-woocommerce' ) );
		}

		$uuid = $channel['uuid'];

		// Delete authentication details first.
		$auth_details_model = new Authentication_Details_Model();
		$auth_details_model->delete_auth_details( $uuid );

		// Delete related product mappings.
		$product_mapping_model = new Product_Mapping_Model();
		$product_mapping_model->delete_mappings_by_channel_id( $uuid );

		// Delete the channel itself.
		$multichannels_model->delete_channel( $channel_id );
	}

	/**
	 * Fetch orders from Amazon via Order_Manager service.
	 * The signature mirrors the parameters previously accepted by the controller.
	 *
	 * @param int    $page             Page number.
	 * @param int    $per_page         Items per page.
	 * @param string $status_filter    Status filter.
	 * @param string $fulfillment_filter Fulfillment filter.
	 * @param string $search           Search query.
	 * @param bool   $fetch_new        Fetch new orders.
	 * @param string $date_from        Date from.
	 * @param string $date_to          Date to.
	 *
	 * @return array<string, mixed> See Order_Manager::fetch_orders() return contract.
	 */
	public function fetch_orders( int $page, int $per_page, string $status_filter = '', string $fulfillment_filter = '', string $search = '', bool $fetch_new = false, string $date_from = '', string $date_to = '' ): array {
		$order_handler = new Order_Handler();

		// Controller previously handled fetch-new branch. We preserve identical behaviour here.
		if ( $fetch_new ) {
			// Retrieve active channels and loop through them to fetch new orders.
			$multichannels_model = new Multichannels_Model();
			$channels            = $multichannels_model->get_all_channels();

			$created = 0;
			$failed  = 0;

			foreach ( $channels as $channel ) {
				try {
					$result   = $order_handler->fetch_new_orders_by_channel( $channel, $date_from, $date_to );
					$created += $result['created'] ?? 0;
					$failed  += $result['failed'] ?? 0;
				} catch ( \Throwable $e ) {
					$this->logger->error( esc_html( 'Failed to fetch new orders for channel: ' . $channel['uuid'] . ': ' . $e->getMessage() ) );
					++$failed;
				}
			}

			return array(
				'created_orders' => $created,
				'failed_orders'  => $failed,
			);
		}

		// If not fetching new orders, delegate to paginated listing.
		return $order_handler->get_orders( $page, $per_page, $status_filter, $fulfillment_filter, $search, $date_from, $date_to );
	}

	/**
	 * Retrieve current sync-queue snapshot.
	 *
	 * Shows individual marketplace syncs with channel/marketplace information.
	 * Also surfaces Amazon → WooCommerce manual product sync sessions by querying
	 * CRON_SYNC_AMAZON_PRODUCTS_BATCH batch-1 jobs (which act as session representatives).
	 *
	 * @param int $page     Page number.
	 * @param int $per_page Items per page.
	 *
	 * @return array<string, mixed>
	 */
	public function get_sync_queue( int $page = 1, int $per_page = 25 ): array {
		if ( ! function_exists( 'as_get_scheduled_actions' ) ) {
			return array(
				'total' => 0,
				'items' => array(),
			);
		}

		$offset            = ( $page - 1 ) * $per_page;
		$amazon_batch_hook = $this->get_amazon_batch_hook();
		$data              = array();

		// ---- WC → Amazon hooks (one AS action = one audit row) ----
		// Include bulk hooks so free-version manual sync (momcs_sync_process_bulk / momcs_unlist_process_bulk) appears in sync audits.
		$wc_hooks = array(
			Plugin_Constants::CRON_SYNC_BULK,
			Plugin_Constants::CRON_UNLIST_BULK,
			Plugin_Constants::CRON_SYNC_SINGLE_MARKETPLACE,
			Plugin_Constants::CRON_UNLIST_SINGLE_MARKETPLACE,
		);

		foreach ( $wc_hooks as $hook ) {
			$actions = as_get_scheduled_actions(
				array(
					'hook'     => $hook,
					'status'   => $this->get_audit_action_statuses(),
					'orderby'  => 'scheduled_date',
					'order'    => 'DESC',
					'per_page' => 100,
				)
			);
			foreach ( $actions as $aid => $aobj ) {
				$args       = $aobj->get_args();
				$raw_status = (string) \ActionScheduler_Store::instance()->get_status( $aid );
				$is_unlist  = in_array( $hook, array( Plugin_Constants::CRON_UNLIST_BULK, Plugin_Constants::CRON_UNLIST_SINGLE_MARKETPLACE ), true );

				// Bulk hooks (free/single-channel) pass product_ids + channel_ids; single-marketplace hooks pass full display args.
				$is_bulk_hook = in_array( $hook, array( Plugin_Constants::CRON_SYNC_BULK, Plugin_Constants::CRON_UNLIST_BULK ), true );
				$mode         = $is_unlist ? 'Unlist' : 'FBM Product Sync';
				if ( $is_bulk_hook ) {
					$channel_ids   = isset( $args['channel_ids'] ) ? (array) $args['channel_ids'] : array();
					$first_channel  = isset( $channel_ids[0] ) ? $channel_ids[0] : '';
					$product_count  = isset( $args['product_ids'] ) ? count( (array) $args['product_ids'] ) : 0;
					$batch_id       = '';
					try {
						$context          = $this->resolve_amazon_sync_action_context( array( 'channel_id' => $first_channel ) );
						$channel_name     = $context['channel_name'];
						$marketplace_name = $context['marketplace_name'];
						if ( ! $is_unlist && ! empty( $context['is_fba'] ) ) {
							$mode = 'FBA Product Sync';
						}
					} catch ( \Throwable $e ) {
						$display = $this->get_channel_display_from_uuid( $first_channel );
						$channel_name     = $display['channel_name'];
						$marketplace_name = $display['marketplace_name'];
					}
				} else {
					$channel_name     = isset( $args['channel_name'] ) ? $args['channel_name'] : '';
					$marketplace_name = isset( $args['marketplace_name'] ) ? $args['marketplace_name'] : '';
					$product_count    = isset( $args['product_ids'] ) ? count( $args['product_ids'] ) : 0;
					$batch_id         = isset( $args['batch_id'] ) ? $args['batch_id'] : '';
				}

				$timestamp = $this->get_action_display_timestamp( (int) $aid, $aobj );

				$data[] = array(
					'id'             => $aid,
					'date'           => $this->format_audit_timestamp( $timestamp ),
					'status'         => $this->get_action_status_label( $raw_status ),
					'type'           => $is_unlist ? 'Unlist' : 'Sync',
					'product_count'  => $product_count,
					'direction'      => 'WooCommerce → Amazon',
					'mode'           => $mode,
					'channel'        => esc_html( (string) $channel_name ),
					'marketplace'    => esc_html( (string) $marketplace_name ),
					'batch_id'       => esc_html( (string) $batch_id ),
					'can_cancel'     => in_array( $raw_status, array( 'pending', 'in-progress' ), true ),
					'sort_timestamp' => $timestamp,
				);
			}
		}

		// ---- Amazon → WooCommerce batch hook ----
		// Group all batch actions by sync_session_id so the UI shows one audit row per
		// sync session even when the import spans multiple batches or channels.
		$batch_actions = as_get_scheduled_actions(
			array(
				'hook'     => $amazon_batch_hook,
				'status'   => $this->get_audit_action_statuses(),
				'orderby'  => 'scheduled_date',
				'order'    => 'ASC',
				'per_page' => 500,
			)
		);

		$amazon_sessions = array();
		foreach ( $batch_actions as $aid => $aobj ) {
			$args       = $aobj->get_args();
			$session_id = (string) ( $args['sync_session_id'] ?? '' );

			if ( '' === $session_id ) {
				continue;
			}

			$context    = $this->resolve_amazon_sync_action_context( $args );
			$raw_status = (string) \ActionScheduler_Store::instance()->get_status( $aid );
			$timestamp  = $this->get_action_display_timestamp( (int) $aid, $aobj );

			if ( ! isset( $amazon_sessions[ $session_id ] ) ) {
				$amazon_sessions[ $session_id ] = array(
					'action_ids'         => array(),
					'channel_names'      => array(),
					'marketplace_names'  => array(),
					'is_fba'             => false,
					'representative_id'  => (int) $aid,
					'representative_ts'  => $timestamp,
					'representative_bat' => (int) ( $args['batch_number'] ?? 0 ),
					'statuses'           => array(),
					'started_timestamp'  => $timestamp,
				);
			}

			$amazon_sessions[ $session_id ]['action_ids'][] = (int) $aid;
			$amazon_sessions[ $session_id ]['statuses'][]   = $raw_status;
			$amazon_sessions[ $session_id ]['is_fba']       = $amazon_sessions[ $session_id ]['is_fba'] || $context['is_fba'];

			if ( '' !== $context['channel_name'] ) {
				$amazon_sessions[ $session_id ]['channel_names'][ $context['channel_name'] ] = $context['channel_name'];
			}

			if ( '' !== $context['marketplace_name'] ) {
				$amazon_sessions[ $session_id ]['marketplace_names'][ $context['marketplace_name'] ] = $context['marketplace_name'];
			}

			if ( 0 === (int) $amazon_sessions[ $session_id ]['started_timestamp'] || ( $timestamp > 0 && $timestamp < (int) $amazon_sessions[ $session_id ]['started_timestamp'] ) ) {
				$amazon_sessions[ $session_id ]['started_timestamp'] = $timestamp;
			}

			$current_batch = (int) ( $args['batch_number'] ?? 0 );
			$current_rep   = (int) $amazon_sessions[ $session_id ]['representative_bat'];
			if (
				( 1 === $current_batch && 1 !== $current_rep ) ||
				( $current_batch === $current_rep && ( 0 === (int) $amazon_sessions[ $session_id ]['representative_ts'] || ( $timestamp > 0 && $timestamp < (int) $amazon_sessions[ $session_id ]['representative_ts'] ) ) )
			) {
				$amazon_sessions[ $session_id ]['representative_id']  = (int) $aid;
				$amazon_sessions[ $session_id ]['representative_ts']  = $timestamp;
				$amazon_sessions[ $session_id ]['representative_bat'] = $current_batch;
			}
		}

		foreach ( $amazon_sessions as $session_id => $session_data ) {
			$started_timestamp = (int) $session_data['started_timestamp'];
			$channel_name      = implode( ', ', array_values( $session_data['channel_names'] ) );
			$marketplace_name  = implode( ', ', array_values( $session_data['marketplace_names'] ) );
			$group_status      = $this->get_grouped_action_status_label( $session_data['statuses'] );

			$data[] = array(
				'id'             => (int) $session_data['representative_id'],
				'date'           => $this->format_audit_timestamp( $started_timestamp ),
				'status'         => $group_status,
				'type'           => 'Sync',
				'product_count'  => 0,
				'direction'      => 'Amazon -> WooCommerce',
				'mode'           => $session_data['is_fba'] ? 'FBA Product Sync' : 'FBM Product Sync',
				'channel'        => esc_html( $channel_name ),
				'marketplace'    => esc_html( $marketplace_name ),
				'batch_id'       => esc_html( (string) $session_id ),
				'can_cancel'     => in_array( $group_status, array( 'Pending', 'In Progress' ), true ),
				'sort_timestamp' => $started_timestamp,
			);
		}

		usort(
			$data,
			static function ( array $left, array $right ): int {
				return (int) ( $right['sort_timestamp'] ?? 0 ) - (int) ( $left['sort_timestamp'] ?? 0 );
			}
		);

		$paged_data = array_slice( $data, $offset, $per_page );
		foreach ( $paged_data as &$item ) {
			unset( $item['sort_timestamp'] );
		}
		unset( $item );

		return array(
			'total' => count( $data ),
			'items' => $paged_data,
		);
	}

	/**
	 * Resolve the Action Scheduler hook used for Amazon → WooCommerce batch syncs.
	 *
	 * @return string
	 */
	private function get_amazon_batch_hook(): string {
		return class_exists( '\MOMCS_Sync\Includes\Plans\Premium\Constants\Plugin_Constants' )
			? \MOMCS_Sync\Includes\Plans\Premium\Constants\Plugin_Constants::CRON_SYNC_AMAZON_PRODUCTS_BATCH
			: 'momcs_premium_sync_amazon_products_batch';
	}

	/**
	 * Convert a raw Action Scheduler status to the UI label used by Sync Audits.
	 *
	 * @param string $raw_status Raw Action Scheduler status.
	 * @return string
	 */
	private function get_action_status_label( string $raw_status ): string {
		switch ( $raw_status ) {
			case \ActionScheduler_Store::STATUS_COMPLETE:
				return 'Completed';
			case \ActionScheduler_Store::STATUS_FAILED:
				return 'Failed';
			case \ActionScheduler_Store::STATUS_CANCELED:
				return 'Cancelled';
			case \ActionScheduler_Store::STATUS_RUNNING:
				return 'In Progress';
			default:
				return ucfirst( $raw_status );
		}
	}

	/**
	 * Collapse multiple Action Scheduler statuses into a single sync-session status.
	 *
	 * @param array<int, string> $raw_statuses Status values across all actions in the session.
	 * @return string
	 */
	private function get_grouped_action_status_label( array $raw_statuses ): string {
		$priority = array( 'failed', 'in-progress', 'pending', 'canceled', 'complete' );

		foreach ( $priority as $status ) {
			if ( in_array( $status, $raw_statuses, true ) ) {
				return $this->get_action_status_label( $status );
			}
		}

		return 'Pending';
	}

	/**
	 * Format a Unix timestamp using the site's date and time format.
	 *
	 * @param int $timestamp Unix timestamp.
	 * @return string
	 */
	private function format_audit_timestamp( int $timestamp ): string {
		if ( $timestamp <= 0 ) {
			return '';
		}

		return esc_html( date_i18n( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $timestamp ) );
	}

	/**
	 * Determine the best available timestamp for an audit action.
	 *
	 * Async actions do not always expose a useful scheduled date, so we fall back to
	 * the earliest Action Scheduler log timestamp when needed.
	 *
	 * @param int   $action_id Action Scheduler action ID.
	 * @param mixed $action    Optional action object.
	 * @return int
	 */
	private function get_action_display_timestamp( int $action_id, $action = null ): int {
		if ( $action && method_exists( $action, 'get_schedule' ) ) {
			$schedule = $action->get_schedule();
			if ( $schedule && method_exists( $schedule, 'get_date' ) ) {
				$date_obj = $schedule->get_date();
				if ( $date_obj ) {
					return (int) $date_obj->getTimestamp();
				}
			}
		}

		return $this->get_action_log_timestamp( $action_id );
	}

	/**
	 * Get the earliest Action Scheduler log timestamp for an action.
	 *
	 * @param int $action_id Action Scheduler action ID.
	 * @return int
	 */
	private function get_action_log_timestamp( int $action_id ): int {
		$logs      = $this->get_action_scheduler_logs( $action_id );
		$timestamp = 0;

		foreach ( $logs as $log ) {
			if ( ! method_exists( $log, 'get_date' ) || ! $log->get_date() ) {
				continue;
			}

			$current_timestamp = (int) $log->get_date()->getTimestamp();
			if ( 0 === $timestamp || $current_timestamp < $timestamp ) {
				$timestamp = $current_timestamp;
			}
		}

		return $timestamp;
	}

	/**
	 * Resolve Amazon sync display metadata from stored action args and channel config.
	 *
	 * @param array<string, mixed> $args Action arguments.
	 * @return array{channel_name:string, marketplace_name:string, is_fba:bool}
	 */
	private function resolve_amazon_sync_action_context( array $args ): array {
		$channel_name    = (string) ( $args['channel_name'] ?? '' );
		$marketplace_id  = '';
		$marketplace     = '';
		$is_fba          = ! empty( $args['is_fba'] );
		$channel_id      = (string) ( $args['channel_id'] ?? '' );
		$mapping_model   = new \MOMCS_Sync\Includes\Plans\Premium\Models\Product_Mapping_Model();
		$channels_model  = new Multichannels_Model();

		if ( '' !== $channel_id ) {
			$channel = $channels_model->get_channel_by_uuid( $channel_id );
			if ( $channel ) {
				if ( '' === $channel_name && ! empty( $channel['connection_name'] ) ) {
					$channel_name = (string) $channel['connection_name'];
				}

				$marketplace_id = (string) ( $channel['region'] ?? '' );
			}

			if ( ! $is_fba ) {
				$sync_settings = $mapping_model->get_sync_settings( $channel_id );
				$account_type  = strtolower( (string) ( $sync_settings['account_settings']['account_type'] ?? 'fbm' ) );
				$is_fba        = ( 'fba' === $account_type );
			}
		}

		if ( '' !== $marketplace_id ) {
			$country_name = Marketplace_IDs::get_country_name_by_marketplace_id( $marketplace_id );
			if ( null !== $country_name ) {
				$marketplace = 'Amazon ' . $country_name;
			}
		}

		return array(
			'channel_name'     => $channel_name,
			'marketplace_name' => $marketplace,
			'is_fba'           => $is_fba,
		);
	}

	/**
	 * Resolve channel display name and marketplace from channel UUID (no premium dependency).
	 * Used for sync audit rows when resolve_amazon_sync_action_context is unavailable (e.g. free-only build).
	 *
	 * @param string $channel_uuid Channel UUID.
	 * @return array{channel_name:string, marketplace_name:string}
	 */
	private function get_channel_display_from_uuid( string $channel_uuid ): array {
		$channel_name    = '';
		$marketplace_name = '';
		if ( '' === $channel_uuid ) {
			return array(
				'channel_name'     => '',
				'marketplace_name' => '',
			);
		}
		$channels_model = new Multichannels_Model();
		$channel        = $channels_model->get_channel_by_uuid( $channel_uuid );
		if ( $channel ) {
			$channel_name    = (string) ( $channel['connection_name'] ?? '' );
			$marketplace_id  = (string) ( $channel['region'] ?? '' );
			if ( '' !== $marketplace_id ) {
				$country_name = Marketplace_IDs::get_country_name_by_marketplace_id( $marketplace_id );
				if ( null !== $country_name ) {
					$marketplace_name = 'Amazon ' . $country_name;
				}
			}
		}
		return array(
			'channel_name'     => $channel_name,
			'marketplace_name' => $marketplace_name,
		);
	}

	/**
	 * Fetch all Action Scheduler logs for a given action ID.
	 *
	 * @param int $action_id Action Scheduler action ID.
	 * @return array<int, object>
	 */
	private function get_action_scheduler_logs( int $action_id ): array {
		if ( ! class_exists( '\ActionScheduler_Logger' ) ) {
			return array();
		}

		$logger = \ActionScheduler_Logger::instance();
		if ( ! method_exists( $logger, 'get_logs' ) ) {
			return array();
		}

		return $logger->get_logs( $action_id );
	}

	/**
	 * Resolve all batch action IDs that belong to the same Amazon sync session.
	 *
	 * @param int $action_id Representative Action Scheduler action ID.
	 * @return array<int, int>
	 */
	private function get_amazon_sync_action_ids( int $action_id ): array {
		if ( ! class_exists( '\ActionScheduler' ) ) {
			return array( $action_id );
		}

		$action = \ActionScheduler::store()->fetch_action( $action_id );
		if ( ! $action || $this->get_amazon_batch_hook() !== $action->get_hook() ) {
			return array( $action_id );
		}

		$args       = $action->get_args();
		$session_id = (string) ( $args['sync_session_id'] ?? '' );
		if ( '' === $session_id ) {
			return array( $action_id );
		}

		$session_actions = as_get_scheduled_actions(
			array(
				'hook'     => $this->get_amazon_batch_hook(),
				'status'   => $this->get_audit_action_statuses(),
				'orderby'  => 'scheduled_date',
				'order'    => 'ASC',
				'per_page' => 500,
			)
		);

		$action_ids = array();
		foreach ( $session_actions as $candidate_id => $candidate_action ) {
			$candidate_args = $candidate_action->get_args();
			if ( (string) ( $candidate_args['sync_session_id'] ?? '' ) === $session_id ) {
				$action_ids[] = (int) $candidate_id;
			}
		}

		if ( empty( $action_ids ) ) {
			return array( $action_id );
		}

		return array_values( array_unique( $action_ids ) );
	}

	/**
	 * Get scheduled sync audits items from Action Scheduler.
	 *
	 * @param int $page Current page number.
	 * @param int $per_page Items per page.
	 * @return array<string, mixed>
	 */
	public function get_scheduled_sync_queue( int $page = 1, int $per_page = 25 ): array {
		if ( ! function_exists( 'as_get_scheduled_actions' ) ) {
			return array(
				'total' => 0,
				'items' => array(),
			);
		}

		// Check if premium constants are available.
		$scheduled_sync_hook = defined( 'MOMCS_Sync\Includes\Plans\Premium\Constants\Plugin_Constants::CRON_SCHEDULED_SYNC' )
			? \MOMCS_Sync\Includes\Plans\Premium\Constants\Plugin_Constants::CRON_SCHEDULED_SYNC
			: 'momcs_scheduled_sync_execute';
		$amazon_to_wc_scheduled_sync_hook = defined( 'MOMCS_Sync\Includes\Plans\Premium\Constants\Plugin_Constants::CRON_SCHEDULED_AMAZON_TO_WC_SYNC' )
			? \MOMCS_Sync\Includes\Plans\Premium\Constants\Plugin_Constants::CRON_SCHEDULED_AMAZON_TO_WC_SYNC
			: 'momcs_scheduled_amazon_to_wc_sync_execute';
		$amazon_to_wc_scheduled_sync_retry_hook = defined( 'MOMCS_Sync\Includes\Plans\Premium\Constants\Plugin_Constants::CRON_SCHEDULED_AMAZON_TO_WC_SYNC_RETRY' )
			? \MOMCS_Sync\Includes\Plans\Premium\Constants\Plugin_Constants::CRON_SCHEDULED_AMAZON_TO_WC_SYNC_RETRY
			: 'momcs_scheduled_amazon_to_wc_sync_retry';
		$fba_stock_scheduled_sync_hook = defined( 'MOMCS_Sync\Includes\Plans\Premium\Constants\Plugin_Constants::CRON_SCHEDULED_FBA_STOCK_SYNC' )
			? \MOMCS_Sync\Includes\Plans\Premium\Constants\Plugin_Constants::CRON_SCHEDULED_FBA_STOCK_SYNC
			: 'momcs_scheduled_fba_stock_sync_execute';

		$offset = ( $page - 1 ) * $per_page;
		$hooks  = array(
			$scheduled_sync_hook,
			$amazon_to_wc_scheduled_sync_hook,
			$amazon_to_wc_scheduled_sync_retry_hook,
			$fba_stock_scheduled_sync_hook,
		);

		$all_actions   = array();
		$total_actions = 0;

		foreach ( $hooks as $hook ) {
			$actions = as_get_scheduled_actions(
				array(
					'hook'     => $hook,
					'status'   => $this->get_audit_action_statuses(),
					'orderby'  => 'scheduled_date',
					'order'    => 'DESC',
					'per_page' => 100,
				)
			);
			foreach ( $actions as $aid => $aobj ) {
				if ( $this->is_fba_stock_child_action( $aid, $hook, $aobj, $fba_stock_scheduled_sync_hook ) ) {
					continue;
				}
				$all_actions[] = array(
					'id'     => $aid,
					'action' => $aobj,
				);
			}
		}
		$total_actions = count( $all_actions );

		// Sort by scheduled date desc with null safety.
		usort(
			$all_actions,
			static function ( $x, $y ) {
				$x_schedule = $x['action']->get_schedule();
				$y_schedule = $y['action']->get_schedule();

				$x_timestamp = 0;
				$y_timestamp = 0;

				if ( $x_schedule ) {
					$x_date = $x_schedule->get_date();
					if ( $x_date ) {
						$x_timestamp = $x_date->getTimestamp();
					}
				}

				if ( $y_schedule ) {
					$y_date = $y_schedule->get_date();
					if ( $y_date ) {
						$y_timestamp = $y_date->getTimestamp();
					}
				}

				return $y_timestamp - $x_timestamp;
			}
		);

		// Apply global pagination.
		$actions = array_slice( $all_actions, $offset, $per_page );

		$data = array();
		foreach ( $actions as $row ) {
			$id     = $row['id'];
			$action = $row['action'];
			$args   = $action->get_args();
			$hook   = $action->get_hook();

			$raw_status = \ActionScheduler_Store::instance()->get_status( $id );
			switch ( $raw_status ) {
				case \ActionScheduler_Store::STATUS_COMPLETE:
					$status = 'Completed';
					break;
				case \ActionScheduler_Store::STATUS_FAILED:
					$status = 'Failed';
					break;
				case \ActionScheduler_Store::STATUS_CANCELED:
					$status = 'Cancelled';
					break;
				case \ActionScheduler_Store::STATUS_RUNNING:
					$status = 'In Progress';
					break;
				default:
					$status = ucfirst( (string) $raw_status );
			}

			$scheduled_date = '';
			$schedule       = $action->get_schedule();
			if ( $schedule && method_exists( $schedule, 'get_date' ) ) {
				$date_obj = $schedule->get_date();
				if ( $date_obj ) {
					$scheduled_date = esc_html( date_i18n( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $date_obj->getTimestamp() ) );
				}
			}

			$is_reverse_schedule = in_array(
				$hook,
				array( $amazon_to_wc_scheduled_sync_hook, $amazon_to_wc_scheduled_sync_retry_hook ),
				true
			);
			$is_fba_stock_schedule = ( $hook === $fba_stock_scheduled_sync_hook );

			if ( $is_reverse_schedule || $is_fba_stock_schedule ) {
				$channel_uuid = '';
				if ( isset( $args['channel_uuid'] ) ) {
					$channel_uuid = (string) $args['channel_uuid'];
				} elseif ( isset( $args[0]['channel_uuid'] ) ) {
					$channel_uuid = (string) $args[0]['channel_uuid'];
				}

				$channel_name = 'Unknown Channel';
				if ( '' !== $channel_uuid ) {
					$channel = ( new Multichannels_Model() )->get_channel_by_uuid( $channel_uuid );
					if ( $channel && ! empty( $channel['connection_name'] ) ) {
						$channel_name = (string) $channel['connection_name'];
					}
				}

				$next_run = '';
				$last_run = '';
				if ( class_exists( '\MOMCS_Sync\Includes\Plans\Premium\Models\Product_Mapping_Model' ) ) {
					$premium_product_mapping_model = new \MOMCS_Sync\Includes\Plans\Premium\Models\Product_Mapping_Model();
					$reverse_schedule_settings     = $is_fba_stock_schedule
						? $premium_product_mapping_model->get_fba_stock_schedule_settings( $channel_uuid )
						: $premium_product_mapping_model->get_amazon_to_wc_schedule_settings( $channel_uuid );
					if ( is_array( $reverse_schedule_settings ) ) {
						$next_run = isset( $reverse_schedule_settings['next_run'] ) ? (string) $reverse_schedule_settings['next_run'] : '';
						$last_run = isset( $reverse_schedule_settings['last_run'] ) ? (string) $reverse_schedule_settings['last_run'] : '';
					}
				}

				$next_run_timestamp = '' !== $next_run ? strtotime( $next_run . ' UTC' ) : false;
				$last_run_timestamp = '' !== $last_run ? strtotime( $last_run . ' UTC' ) : false;
				$next_run_display   = $next_run_timestamp
					? esc_html( date_i18n( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $next_run_timestamp ) )
					: '';
				$last_run_display   = $last_run_timestamp
					? esc_html( date_i18n( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $last_run_timestamp ) )
					: '';

				$data[] = array(
					'id'        => $id,
					'channel'   => esc_html( $channel_name ),
					'direction' => 'Amazon -> WooCommerce',
					'mode'      => $is_fba_stock_schedule ? 'FBA Stock Sync' : 'Full Import',
					'next_run'  => $next_run_display,
					'last_run'  => $last_run_display,
					'date'      => $scheduled_date,
					'status'    => $status,
					'type'      => 'Scheduled Sync',
				);
			} else {
				// Get schedule info for existing scheduled attribute sync rows.
				$schedule_info = isset( $args[0] ) && is_array( $args[0] ) ? $args[0] : $args;
				$channel_name  = isset( $schedule_info['channel_name'] ) ? (string) $schedule_info['channel_name'] : 'Unknown Channel';
				$attributes    = isset( $schedule_info['selected_attributes'] ) && is_array( $schedule_info['selected_attributes'] )
					? implode( ', ', $schedule_info['selected_attributes'] )
					: 'All attributes';

				$data[] = array(
					'id'         => $id,
					'channel'    => esc_html( $channel_name ),
					'attributes' => esc_html( $attributes ),
					'date'       => $scheduled_date,
					'status'     => $status,
					'type'       => 'Scheduled Sync',
				);
			}
		}

		return array(
			'total' => $total_actions,
			'items' => $data,
		);
	}

	/**
	 * Check whether an action is a child FBA stock pagination batch.
	 *
	 * @param int    $action_id            Action Scheduler action ID.
	 * @param string $hook                 Action Scheduler hook name.
	 * @param object $action               Action Scheduler action instance.
	 * @param string $fba_stock_sync_hook  Hook name for FBA stock sync.
	 * @return bool True when the action is a child pagination batch.
	 */
	private function is_fba_stock_child_action( int $action_id, string $hook, $action, string $fba_stock_sync_hook ): bool {
		if ( $hook !== $fba_stock_sync_hook || ! is_object( $action ) || ! method_exists( $action, 'get_args' ) ) {
			return false;
		}

		$args = $action->get_args();
		$pagination_context = array();
		if ( isset( $args['pagination_context'] ) && is_array( $args['pagination_context'] ) ) {
			$pagination_context = $args['pagination_context'];
		} elseif ( isset( $args[0]['pagination_context'] ) && is_array( $args[0]['pagination_context'] ) ) {
			$pagination_context = $args[0]['pagination_context'];
		}

		if ( empty( $pagination_context['parent_action_id'] ) ) {
			return false;
		}

		$parent_action_id = (int) $pagination_context['parent_action_id'];
		return $parent_action_id > 0 && $parent_action_id !== $action_id;
	}

	/**
	 * Retrieve logs for a background job.
	 *
	 * @param string $job_id Job identifier.
	 *
	 * @return array<string, mixed>
	 */
	public function get_job_logs( string $job_id ): array {
		if ( ! function_exists( 'as_get_scheduled_actions' ) ) {
			return array();
		}

		$action_ids = $this->get_amazon_sync_action_ids( (int) $job_id );
		$data       = array();

		foreach ( $action_ids as $action_id ) {
			$logs = $this->get_action_scheduler_logs( $action_id );
			foreach ( $logs as $log ) {
				$msg = $log->get_message();
				// Remove bracketed prefixes like [INFO][1234].
				$body    = preg_replace( '/^(\[[^\]]*\]\s*){1,2}/', '', $msg );
				$decoded = json_decode( $body, true );
				if ( JSON_ERROR_NONE === json_last_error() && is_array( $decoded ) ) {
					$payload = $decoded;
					// Enrich payload with product title when we have a product ID
					// so the UI can show human-friendly names in tabs.
					if ( isset( $payload['product_id'] ) && ! isset( $payload['product_title'] ) ) {
						$product = wc_get_product( (int) $payload['product_id'] );
						if ( $product ) {
							$payload['product_title'] = $product->get_name();
						}
					}
					$pretty = wp_json_encode( $decoded, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES );
				} else {
					$payload = null;
					$pretty  = esc_html( $body );
				}
				if ( ! $payload || ! isset( $payload['stage'] ) ) {
					// Skip unstructured log rows.
					continue;
				}
				// Redact sensitive tokens from headers before sending to frontend.
				$payload = $this->sanitize_log_payload( $payload );
				if ( 0 === strpos( $msg, '[REQUEST]' ) ) {
					$type = 'request';
				} elseif ( 0 === strpos( $msg, '[RESPONSE]' ) ) {
					$type = 'response';
				} elseif ( false !== strpos( $msg, '[ERROR]' ) ) {
					$type = 'error';
				} elseif ( false !== strpos( $msg, '[WARNING]' ) ) {
					$type = 'warning';
				} elseif ( false !== strpos( $msg, '[SUCCESS]' ) ) {
					$type = 'success';
				} else {
					$type = 'info';
				}
				$data[] = array(
					'time'    => $log->get_date()->format( 'Y-m-d H:i:s' ),
					'type'    => $type,
					'pretty'  => $pretty,
					'payload' => $payload,
				);
			}
		}

		usort(
			$data,
			static function ( array $left, array $right ): int {
				return strcmp( $left['time'], $right['time'] );
			}
		);

		return $data;
	}

	/**
	 * Redact sensitive values (access tokens, authorization headers) from a log payload.
	 *
	 * @param array<string, mixed> $payload Decoded log payload.
	 * @return array<string, mixed> Sanitized payload.
	 */
	private function sanitize_log_payload( array $payload ): array {
		if ( isset( $payload['headers'] ) && is_array( $payload['headers'] ) ) {
			$sensitive = array( 'x-amz-access-token', 'authorization' );
			foreach ( $payload['headers'] as $key => $value ) {
				if ( in_array( strtolower( (string) $key ), $sensitive, true ) ) {
					$payload['headers'][ $key ] = '***';
				}
			}
		}
		return $payload;
	}

	/**
	 * Get order sync audits items from Action Scheduler.
	 *
	 * @param int    $page      Current page number.
	 * @param int    $per_page  Items per page.
	 * @param string $sync_type Filter by sync type ('manual', 'scheduled', or '' for all).
	 * @return array<string, mixed>
	 */
	public function get_order_sync_queue( int $page = 1, int $per_page = 25, string $sync_type = '' ): array {
		if ( ! function_exists( 'as_get_scheduled_actions' ) ) {
			return array(
				'total' => 0,
				'items' => array(),
			);
		}

		$offset = ( $page - 1 ) * $per_page;
		$hook   = Plugin_Constants::CRON_FETCH_ORDERS;

		// Get all order fetch jobs from Action Scheduler.
		$ids           = as_get_scheduled_actions(
			array(
				'hook'     => $hook,
				'status'   => array( 'pending', 'in-progress', 'complete', 'failed', 'canceled' ),
				'orderby'  => 'scheduled_date',
				'order'    => 'DESC',
				'per_page' => 100, // Get more items to properly paginate.
			),
			'ids'
		);
		$total_actions = count( $ids );

		$actions = as_get_scheduled_actions(
			array(
				'hook'     => $hook,
				'status'   => $this->get_audit_action_statuses(),
				'orderby'  => 'scheduled_date',
				'order'    => 'DESC',
				'per_page' => 100,
			)
		);

		$all_actions = array();
		foreach ( $actions as $aid => $aobj ) {
			$all_actions[] = array(
				'id'     => $aid,
				'action' => $aobj,
			);
		}

		// Sort by scheduled date desc with null safety.
		usort(
			$all_actions,
			static function ( $x, $y ) {
				$x_schedule = $x['action']->get_schedule();
				$y_schedule = $y['action']->get_schedule();

				$x_timestamp = 0;
				$y_timestamp = 0;

				if ( $x_schedule ) {
					$x_date = $x_schedule->get_date();
					if ( $x_date ) {
						$x_timestamp = $x_date->getTimestamp();
					}
				}

				if ( $y_schedule ) {
					$y_date = $y_schedule->get_date();
					if ( $y_date ) {
						$y_timestamp = $y_date->getTimestamp();
					}
				}

				return $y_timestamp - $x_timestamp;
			}
		);

		// Apply global pagination.
		$actions = array_slice( $all_actions, $offset, $per_page );

		$data = array();
		foreach ( $actions as $row ) {
			$id     = $row['id'];
			$action = $row['action'];
			$args   = $action->get_args();

			// Extract order sync info from action args.
			$parent_job_id = $args['parent_job_id'] ?? '';
			$channel_uuid  = $args['channel_uuid'] ?? '';
			$batch_number  = $args['batch_number'] ?? 1;

			// Try to get enriched metadata from transient if available.
			$metadata       = $parent_job_id ? get_transient( 'momcs_order_job_' . $parent_job_id ) : false;
			$item_sync_type = $metadata ? ( $metadata['sync_type'] ?? 'manual' ) : 'manual';

			// Filter by sync type when requested.
			if ( '' !== $sync_type && $item_sync_type !== $sync_type ) {
				continue;
			}

			$fetched = $metadata ? ( $metadata['total_fetched'] ?? 0 ) : 0;
			$created   = $metadata ? ( $metadata['total_created'] ?? 0 ) : 0;
			$failed    = $metadata ? ( $metadata['total_failed'] ?? 0 ) : 0;
			$skipped   = $metadata ? ( $metadata['total_skipped'] ?? 0 ) : 0;

			// Calculate date range.
			$date_range = '';
			if ( $metadata && ! empty( $metadata['date_from'] ) && ! empty( $metadata['date_to'] ) ) {
				$date_range = gmdate( 'Y-m-d', strtotime( $metadata['date_from'] ) ) . ' to ' . gmdate( 'Y-m-d', strtotime( $metadata['date_to'] ) );
			}

			$raw_status = \ActionScheduler_Store::instance()->get_status( $id );
			switch ( $raw_status ) {
				case \ActionScheduler_Store::STATUS_COMPLETE:
					$status = 'Completed';
					break;
				case \ActionScheduler_Store::STATUS_FAILED:
					$status = 'Failed';
					break;
				case \ActionScheduler_Store::STATUS_CANCELED:
					$status = 'Cancelled';
					break;
				case \ActionScheduler_Store::STATUS_RUNNING:
					$status = 'In Progress';
					break;
				default:
					$status = ucfirst( (string) $raw_status );
			}

			// Only include batch 1 jobs to avoid duplicates (each batch creates a separate job).
			if ( 1 === $batch_number ) {
				// Get scheduled date safely with null checks.
				$scheduled_date = '';
				$schedule       = $action->get_schedule();
				if ( $schedule && method_exists( $schedule, 'get_date' ) ) {
					$date_obj = $schedule->get_date();
					if ( $date_obj ) {
						$scheduled_date = esc_html( date_i18n( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $date_obj->getTimestamp() ) );
					}
				}

				// Fallback to metadata started_at if schedule date not available.
				if ( empty( $scheduled_date ) && $metadata && ! empty( $metadata['started_at'] ) ) {
					$scheduled_date = esc_html( date_i18n( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), strtotime( $metadata['started_at'] ) ) );
				}

				$data[] = array(
					'id'         => $id,
					'sync_type'  => esc_html( $item_sync_type ),
					'date_range' => esc_html( $date_range ),
					'fetched'    => $fetched,
					'created'    => $created,
					'failed'     => $failed,
					'skipped'    => $skipped,
					'date'       => $scheduled_date,
					'status'     => $status,
				);
			}
		}

		return array(
			'total' => count( $data ), // Use filtered count.
			'items' => $data,
		);
	}

	/**
	 * Retrieve all channels currently stored in the momcs_multichannels table.
	 *
	 * @return array<int, array<string, mixed>>
	 */
	public function get_all_channels(): array {
		$multichannels_model = new Multichannels_Model();
		return $multichannels_model->get_all_channels();
	}
}
