<?php
/**
 * Class Noesia Cs Reorder Service.
 *
 * @package Noesia_Commerce_Suite
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Class implementation.
 */
class Noesia_CS_Reorder_Service {
	const QUEUE_OPTION     = 'noesia_cs_reorder_queue';
	const LOG_OPTION       = 'noesia_cs_reorder_logs';
	const SENT_OPTION      = 'noesia_cs_reorder_sent_keys';
	const META_OPT_OUT     = '_noesia_cs_reorder_opt_out';
	const META_OPT_OUT_AT  = '_noesia_cs_reorder_opt_out_at';
	const META_OPT_OUT_SOURCE = '_noesia_cs_reorder_opt_out_source';
	const META_CHECKOUT_REQUESTED = '_noesia_cs_reorder_requested';
	const META_CHECKOUT_REQUESTED_DATE = '_noesia_cs_reorder_requested_date';
	const META_CHECKOUT_REQUESTED_DUE_AT = '_noesia_cs_reorder_requested_due_at';
	const META_CHECKOUT_REQUESTED_AT = '_noesia_cs_reorder_requested_at';
	const CRON_HOOK        = 'noesia_cs_process_reorder_reminders';
	const BUILD_LOCK       = 'noesia_cs_reorder_build_lock';
	const PROCESS_LOCK     = 'noesia_cs_reorder_process_lock';

	/**
	 * Runtime dependency or state value.
	 *
	 * @var mixed
	 */
	private $passport_service;
	/**
	 * Runtime dependency or state value.
	 *
	 * @var mixed
	 */
	private $email_service;
	/**
	 * Runtime dependency or state value.
	 *
	 * @var mixed
	 */
	private $order_repository;

	/**
	 * Handles the related plugin operation.
	 *
	 * @param mixed $passport_service Passport service.
	 * @param mixed $email_service Email service.
	 * @param mixed $order_repository Order repository.
	 */
	public function __construct( Noesia_CS_Passport_Service $passport_service, Noesia_CS_Email_Service $email_service = null, Noesia_CS_Order_Repository $order_repository = null ) {
		$this->passport_service  = $passport_service;
		$this->email_service     = $email_service;
		$this->order_repository = $order_repository ? $order_repository : new Noesia_CS_Order_Repository();
	}

	/**
	 * Handles the related plugin operation.
	 *
	 * @param mixed $email_service Email service.
	 */
	public function set_email_service( Noesia_CS_Email_Service $email_service ) {
		$this->email_service = $email_service;
	}

	/**
	 * Handles the related plugin operation.
	 */
	public function run_scheduled_cycle() {
		$settings = Noesia_CS_Settings::get_all();
		if ( 'yes' !== $settings['reorder_reminders_enabled'] || 'yes' !== $settings['reorder_email_enabled'] ) {
			return array(
				'built'       => array( 'orders_checked' => 0, 'queued' => 0, 'skipped' => 0, 'inactive' => true ),
				'processed'   => array( 'sent' => 0, 'failed' => 0, 'skipped' => 0, 'processed' => 0, 'inactive' => true ),
			);
		}

		$built = array(
			'orders_checked'  => 0,
			'queued'          => 0,
			'skipped'         => 0,
			'checkout_only'   => true,
		);

		return array(
			'built'      => $built,
			'processed'  => $this->process_due_queue(),
		);
	}

	/**
	 * Handles the related plugin operation.
	 *
	 * @param mixed $order_limit Order limit.
	 */
	public function build_due_queue( $order_limit = 200 ) {
		$this->add_log( 'skipped', 0, 0, __( 'Automatic historical reminder building is disabled. Customer-requested checkout reminders are saved when the order is placed.', 'noesia-commerce-suite' ) );
		return array(
			'orders_checked'  => 0,
			'queued'          => 0,
			'skipped'         => 0,
			'inactive'        => false,
			'checkout_only'   => true,
		);
	}

	/**
	 * Handles the related plugin operation.
	 *
	 * @param mixed $order Order.
	 * @param mixed $due_at Due at.
	 */
	public function queue_customer_requested_reminder( $order, $due_at ) {
		if ( ! $order || ! is_a( $order, 'WC_Order' ) ) {
			return new WP_Error( 'noesia_cs_invalid_order', __( 'A valid WooCommerce order is required.', 'noesia-commerce-suite' ) );
		}

		$due_at = absint( $due_at );
		if ( ! $due_at || $due_at < time() ) {
			return new WP_Error( 'noesia_cs_invalid_due_date', __( 'A valid future reminder date is required.', 'noesia-commerce-suite' ) );
		}

		if ( $this->is_order_opted_out( $order ) ) {
			$this->add_log( 'skipped', absint( $order->get_id() ), 0, __( 'Checkout reminder request skipped because the order opted out.', 'noesia-commerce-suite' ) );
			return false;
		}

		$email = sanitize_email( $order->get_billing_email() );
		if ( ! $email ) {
			$this->add_log( 'skipped', absint( $order->get_id() ), 0, __( 'Checkout reminder request skipped because the order has no valid billing email.', 'noesia-commerce-suite' ) );
			return false;
		}

		$item = $this->get_first_reorderable_item( $order );
		if ( ! $item ) {
			$this->add_log( 'skipped', absint( $order->get_id() ), 0, __( 'Checkout reminder request skipped because no reorderable item was found.', 'noesia-commerce-suite' ) );
			return false;
		}

		$product = method_exists( $item, 'get_product' ) ? $item->get_product() : null;
		if ( ! $product ) {
			$this->add_log( 'skipped', absint( $order->get_id() ), absint( $item->get_id() ), __( 'Checkout reminder request skipped because the product is unavailable.', 'noesia-commerce-suite' ) );
			return false;
		}

		$queue      = $this->get_queue();
		$sent_keys  = $this->get_sent_keys();
		$key        = $this->build_key( $order->get_id(), $item->get_id(), $product->get_id(), $due_at );

		if ( isset( $queue[ $key ] ) || isset( $sent_keys[ $key ] ) ) {
			$this->add_log( 'skipped', absint( $order->get_id() ), absint( $item->get_id() ), __( 'Checkout reminder request already exists for this order and date.', 'noesia-commerce-suite' ) );
			return false;
		}

		$queue[ $key ] = array(
			'key'             => $key,
			'order_id'        => absint( $order->get_id() ),
			'item_id'         => absint( $item->get_id() ),
			'product_id'      => absint( $product->get_id() ),
			'product_name'    => sanitize_text_field( $item->get_name() ),
			'customer_email'  => $email,
			'due_at'          => $due_at,
			'reminder_days'   => 0,
			'rule_source'     => 'checkout_request',
			'status'          => 'pending',
			'created_at'      => time(),
			'updated_at'      => time(),
			'last_error'      => '',
		);

		$this->save_queue( $queue );
		$this->add_log( 'scheduled', absint( $order->get_id() ), absint( $item->get_id() ), sprintf( /* translators: %s: reminder date. */ __( 'Customer reminder saved for %s.', 'noesia-commerce-suite' ), date_i18n( get_option( 'date_format' ), $due_at ) ) );
		return true;
	}

	/**
	 * Handles the related plugin operation.
	 *
	 * @param mixed $order Order.
	 */
	private function get_first_reorderable_item( $order ) {
		foreach ( $order->get_items() as $item ) {
			if ( ! method_exists( $item, 'get_product' ) ) {
				continue;
			}
			$product = $item->get_product();
			if ( ! $product ) {
				continue;
			}
			return $item;
		}
		return null;
	}

	/**
	 * Handles the related plugin operation.
	 *
	 * @param mixed $limit Limit.
	 */
	public function process_due_queue( $limit = 0 ) {
		$settings = Noesia_CS_Settings::get_all();
		if ( 'yes' !== $settings['reorder_reminders_enabled'] || 'yes' !== $settings['reorder_email_enabled'] ) {
			$this->add_log( 'skipped', 0, 0, __( 'Reminder processing skipped because reorder reminders or reorder email is disabled.', 'noesia-commerce-suite' ) );
			return array(
				'sent'       => 0,
				'failed'     => 0,
				'skipped'    => 0,
				'processed'  => 0,
				'inactive'   => true,
				'locked'     => false,
			);
		}

		if ( ! $this->email_service ) {
			$this->add_log( 'failed', 0, 0, __( 'Email service is unavailable.', 'noesia-commerce-suite' ) );
			return array(
				'sent'       => 0,
				'failed'     => 1,
				'skipped'    => 0,
				'processed'  => 0,
				'inactive'   => false,
				'locked'     => false,
			);
		}

		if ( ! $this->acquire_lock( self::PROCESS_LOCK, 10 * MINUTE_IN_SECONDS ) ) {
			$this->add_log( 'skipped', 0, 0, __( 'Reminder sending is already running. Try again shortly.', 'noesia-commerce-suite' ) );
			return array(
				'sent'       => 0,
				'failed'     => 0,
				'skipped'    => 0,
				'processed'  => 0,
				'inactive'   => false,
				'locked'     => true,
			);
		}

		$limit      = $limit ? absint( $limit ) : absint( $settings['reorder_batch_size'] );
		$limit      = max( 1, min( 100, $limit ) );
		$now        = time();
		$queue      = $this->get_queue();
		$sent_keys  = $this->get_sent_keys();
		$sent       = 0;
		$failed     = 0;
		$skipped    = 0;
		$processed  = 0;

		foreach ( $queue as $key => $entry ) {
			if ( $processed >= $limit ) {
				break;
			}
			if ( ! is_array( $entry ) || 'pending' !== $entry['status'] || absint( $entry['due_at'] ) > $now ) {
				continue;
			}

			++$processed;
			$order = function_exists( 'wc_get_order' ) ? wc_get_order( absint( $entry['order_id'] ) ) : null;
			if ( ! $order ) {
				$queue[ $key ]['status']     = 'failed';
				$queue[ $key ]['last_error'] = __( 'Order could not be loaded.', 'noesia-commerce-suite' );
				$queue[ $key ]['updated_at'] = time();
				++$failed;
				$this->add_log( 'failed', absint( $entry['order_id'] ), absint( $entry['item_id'] ), $queue[ $key ]['last_error'] );
				continue;
			}

			if ( $this->is_order_opted_out( $order ) ) {
				$queue[ $key ]['status']     = 'skipped';
				$queue[ $key ]['last_error'] = __( 'Customer/order opted out.', 'noesia-commerce-suite' );
				$queue[ $key ]['updated_at'] = time();
				++$skipped;
				$this->add_log( 'skipped', absint( $entry['order_id'] ), absint( $entry['item_id'] ), $queue[ $key ]['last_error'] );
				continue;
			}

			$item = $order->get_item( absint( $entry['item_id'] ) );
			if ( ! $item ) {
				$queue[ $key ]['status']     = 'failed';
				$queue[ $key ]['last_error'] = __( 'Order item could not be loaded.', 'noesia-commerce-suite' );
				$queue[ $key ]['updated_at'] = time();
				++$failed;
				$this->add_log( 'failed', absint( $entry['order_id'] ), absint( $entry['item_id'] ), $queue[ $key ]['last_error'] );
				continue;
			}

			$unsubscribe_url  = $this->get_unsubscribe_url_for_order( $order );
			$result           = $this->email_service->send_reorder_reminder( $order, $item, $unsubscribe_url );
			if ( is_wp_error( $result ) ) {
				$queue[ $key ]['status']     = 'failed';
				$queue[ $key ]['last_error'] = $result->get_error_message();
				$queue[ $key ]['updated_at'] = time();
				++$failed;
				$this->add_log( 'failed', absint( $entry['order_id'] ), absint( $entry['item_id'] ), $result->get_error_message() );
				continue;
			}

			$queue[ $key ]['status']     = 'sent';
			$queue[ $key ]['last_error'] = '';
			$queue[ $key ]['sent_at']    = time();
			$queue[ $key ]['updated_at'] = time();
			$sent_keys[ $key ]           = time();
			++$sent;
			$this->add_log( 'sent', absint( $entry['order_id'] ), absint( $entry['item_id'] ), __( 'Reorder reminder email sent.', 'noesia-commerce-suite' ) );
		}

		$this->save_queue( $queue );
		$this->save_sent_keys( $sent_keys );
		$this->release_lock( self::PROCESS_LOCK );

		return array(
			'sent'       => $sent,
			'failed'     => $failed,
			'skipped'    => $skipped,
			'processed'  => $processed,
			'inactive'   => false,
			'locked'     => false,
		);
	}



	/**
	 * Handles the related plugin operation.
	 *
	 * @param mixed $queue_key Queue key.
	 * @param mixed $recipient Recipient.
	 */
	public function send_test_copy_to_admin( $queue_key, $recipient = '' ) {
		$queue_key = sanitize_text_field( $queue_key );
		if ( '' === $queue_key ) {
			$this->add_log( 'failed', 0, 0, __( 'Reorder reminder test copy failed because no reminder was selected.', 'noesia-commerce-suite' ) );
			return new WP_Error( 'noesia_cs_missing_queue_key', __( 'Select a reminder first.', 'noesia-commerce-suite' ) );
		}

		$queue = $this->get_queue();
		if ( ! isset( $queue[ $queue_key ] ) || ! is_array( $queue[ $queue_key ] ) ) {
			$this->add_log( 'failed', 0, 0, __( 'Reorder reminder test copy failed because the reminder could not be found.', 'noesia-commerce-suite' ) );
			return new WP_Error( 'noesia_cs_missing_queue_item', __( 'The selected reminder could not be found.', 'noesia-commerce-suite' ) );
		}

		return $this->send_test_copy_for_queue_entry( $queue[ $queue_key ], $recipient );
	}

	/**
	 * Handles the related plugin operation.
	 *
	 * @param mixed $recipient Recipient.
	 */
	public function send_sample_test_copy_to_admin( $recipient = '' ) {
		if ( ! function_exists( 'wc_get_orders' ) ) {
			return new WP_Error( 'noesia_cs_woocommerce_unavailable', __( 'WooCommerce orders are unavailable.', 'noesia-commerce-suite' ) );
		}

		$orders = wc_get_orders(
			array(
				'limit'    => 10,
				'orderby'  => 'date',
				'order'    => 'DESC',
				'status'   => array( 'wc-completed', 'wc-processing', 'wc-on-hold', 'wc-pending' ),
				'return'   => 'objects',
			)
		);

		foreach ( $orders as $order ) {
			if ( ! $order || ! is_a( $order, 'WC_Order' ) ) {
				continue;
			}
			$item = $this->get_first_reorderable_item( $order );
			if ( ! $item ) {
				continue;
			}
			$entry = array(
				'order_id'      => absint( $order->get_id() ),
				'item_id'       => absint( $item->get_id() ),
				'due_at'        => time(),
				'product_name'  => sanitize_text_field( $item->get_name() ),
			);
			return $this->send_test_copy_for_queue_entry( $entry, $recipient, true );
		}

		$this->add_log( 'failed', 0, 0, __( 'Reorder reminder sample test failed because no recent order with a reorderable item was found.', 'noesia-commerce-suite' ) );
		return new WP_Error( 'noesia_cs_missing_sample_order', __( 'No recent order with a reorderable item was found.', 'noesia-commerce-suite' ) );
	}

	/**
	 * Handles the related plugin operation.
	 *
	 * @param mixed $entry Entry.
	 * @param mixed $recipient Recipient.
	 * @param mixed $is_sample Is sample.
	 */
	private function send_test_copy_for_queue_entry( $entry, $recipient = '', $is_sample = false ) {
		if ( ! $this->email_service ) {
			$this->add_log( 'failed', 0, 0, __( 'Reorder reminder test copy failed because the email service is unavailable.', 'noesia-commerce-suite' ) );
			return new WP_Error( 'noesia_cs_email_service_unavailable', __( 'Email service is unavailable.', 'noesia-commerce-suite' ) );
		}

		$order = function_exists( 'wc_get_order' ) ? wc_get_order( absint( $entry['order_id'] ?? 0 ) ) : null;
		if ( ! $order ) {
			$this->add_log( 'failed', absint( $entry['order_id'] ?? 0 ), 0, __( 'Reorder reminder test copy failed because the order could not be loaded.', 'noesia-commerce-suite' ) );
			return new WP_Error( 'noesia_cs_missing_test_order', __( 'The order could not be loaded.', 'noesia-commerce-suite' ) );
		}

		$item = $order->get_item( absint( $entry['item_id'] ?? 0 ) );
		if ( ! $item ) {
			$this->add_log( 'failed', absint( $entry['order_id'] ?? 0 ), absint( $entry['item_id'] ?? 0 ), __( 'Reorder reminder test copy failed because the order item could not be loaded.', 'noesia-commerce-suite' ) );
			return new WP_Error( 'noesia_cs_missing_test_item', __( 'The order item could not be loaded.', 'noesia-commerce-suite' ) );
		}

		$unsubscribe_url  = $this->get_unsubscribe_url_for_order( $order );
		$result           = $this->email_service->send_reorder_reminder_test_copy( $order, $item, $unsubscribe_url, $recipient );
		if ( is_wp_error( $result ) ) {
			return $result;
		}

		$message = $is_sample ? __( 'Test reminder email sent. No customer reminders were changed.', 'noesia-commerce-suite' ) : __( 'Test reminder email sent. No customer reminders were changed.', 'noesia-commerce-suite' );
		$this->add_log( 'sent', absint( $entry['order_id'] ?? 0 ), absint( $entry['item_id'] ?? 0 ), $message );
		return true;
	}

	/**
	 * Handles the related plugin operation.
	 */
	public function get_runtime_status() {
		$build_started    = absint( get_transient( self::BUILD_LOCK ) );
		$process_started  = absint( get_transient( self::PROCESS_LOCK ) );
		$next_cron        = wp_next_scheduled( self::CRON_HOOK );
		$queue            = $this->get_queue();
		$pending_due      = 0;
		$pending_future   = 0;
		$now              = time();

		foreach ( $queue as $entry ) {
			if ( ! is_array( $entry ) || 'pending' !== ( $entry['status'] ?? 'pending' ) ) {
				continue;
			}
			$due_at = absint( $entry['due_at'] ?? 0 );
			if ( $due_at && $due_at <= $now ) {
				++$pending_due;
			} else {
				++$pending_future;
			}
		}

		$logs        = get_option( self::LOG_OPTION, array() );
		$logs        = is_array( $logs ) ? $logs : array();
		$last_log    = isset( $logs[0] ) && is_array( $logs[0] ) ? $logs[0] : array();
		$settings    = Noesia_CS_Settings::get_all();
		$mail_ready  = 'yes' === ( $settings['reorder_reminders_enabled'] ?? 'no' ) && 'yes' === ( $settings['reorder_email_enabled'] ?? 'no' );

		return array(
			'build_locked'        => $build_started > 0,
			'build_started_at'    => $build_started,
			'process_locked'      => $process_started > 0,
			'process_started_at'  => $process_started,
			'next_cron'           => $next_cron ? absint( $next_cron ) : 0,
			'pending_due'         => $pending_due,
			'pending_future'      => $pending_future,
			'email_engine_ready'  => $mail_ready,
			'last_status'         => isset( $last_log['status'] ) ? sanitize_key( $last_log['status'] ) : '',
			'last_message'        => isset( $last_log['message'] ) ? sanitize_text_field( $last_log['message'] ) : '',
			'last_time'           => isset( $last_log['time'] ) ? absint( $last_log['time'] ) : 0,
		);
	}

	/**
	 * Handles the related plugin operation.
	 *
	 * @param mixed $key Key.
	 * @param mixed $ttl Ttl.
	 */
	private function acquire_lock( $key, $ttl ) {
		$key = sanitize_key( $key );
		if ( get_transient( $key ) ) {
			return false;
		}
		set_transient( $key, time(), max( MINUTE_IN_SECONDS, absint( $ttl ) ) );
		return true;
	}

	/**
	 * Handles the related plugin operation.
	 *
	 * @param mixed $key Key.
	 */
	private function release_lock( $key ) {
		delete_transient( sanitize_key( $key ) );
	}


	/**
	 * Handles the related plugin operation.
	 *
	 * @param mixed $default_days Default days.
	 * @param mixed $order Order.
	 * @param mixed $item Item.
	 * @param mixed $product Product.
	 */
	private function get_reminder_days_for_item( $default_days, $order, $item, $product ) {
		$default_days  = max( 1, absint( $default_days ) );
		$product_id    = $product && method_exists( $product, 'get_id' ) ? absint( $product->get_id() ) : 0;
		$days          = apply_filters( 'noesia_cs_item_reorder_days', $default_days, $order, $item, $product_id, $product );
		return absint( $days );
	}

	/**
	 * Handles the related plugin operation.
	 *
	 * @param mixed $order Order.
	 * @param mixed $source Source.
	 */
	public function opt_out_order( $order, $source = 'public' ) {
		if ( ! $order || ! is_a( $order, 'WC_Order' ) ) {
			return false;
		}
		$order->update_meta_data( self::META_OPT_OUT, 'yes' );
		$order->update_meta_data( self::META_OPT_OUT_AT, time() );
		$order->update_meta_data( self::META_OPT_OUT_SOURCE, sanitize_text_field( $source ) );
		$order->save();
		$this->add_log( 'opted_out', absint( $order->get_id() ), 0, sprintf( /* translators: %s: opt-out source. */ __( 'Reorder reminder opt-out registered from %s.', 'noesia-commerce-suite' ), sanitize_text_field( $source ) ) );
		return true;
	}

	/**
	 * Handles the related plugin operation.
	 *
	 * @param mixed $order Order.
	 */
	public function is_order_opted_out( $order ) {
		if ( ! $order || ! is_a( $order, 'WC_Order' ) ) {
			return true;
		}
		return 'yes' === (string) $order->get_meta( self::META_OPT_OUT, true );
	}

	/**
	 * Handles the related plugin operation.
	 *
	 * @param mixed $order Order.
	 */
	public function get_unsubscribe_url_for_order( $order ) {
		$token = $this->passport_service->get_existing_token_for_order( $order );
		if ( ! $token ) {
			$token = $this->passport_service->get_or_create_token_for_order( $order );
		}
		if ( ! $token ) {
			return '';
		}
		return home_url( user_trailingslashit( 'noesia-reorder-opt-out/' . rawurlencode( $token ) ) );
	}

	/**
	 * Handles the related plugin operation.
	 */
	public function get_queue() {
		$queue = get_option( self::QUEUE_OPTION, array() );
		return is_array( $queue ) ? $queue : array();
	}

	/**
	 * Handles the related plugin operation.
	 */
	public function get_queue_counts() {
		$counts = array(
			'pending'    => 0,
			'sent'       => 0,
			'failed'     => 0,
			'skipped'    => 0,
			'opted_out'  => 0,
		);
		foreach ( $this->get_queue() as $entry ) {
			$status = is_array( $entry ) && isset( $entry['status'] ) ? sanitize_key( $entry['status'] ) : 'pending';
			if ( ! isset( $counts[ $status ] ) ) {
				$counts[ $status ] = 0;
			}
			$counts[ $status ]++;
		}
		$counts['opted_out'] = count( $this->get_recent_opt_outs( 1000 ) );
		return $counts;
	}


	/**
	 * Handles the related plugin operation.
	 */
	public function get_unique_pending_customer_count() {
		$customers = array();
		foreach ( $this->get_queue() as $entry ) {
			if ( ! is_array( $entry ) ) {
				continue;
			}
			$status = isset( $entry['status'] ) ? sanitize_key( $entry['status'] ) : 'pending';
			if ( 'pending' !== $status ) {
				continue;
			}
			$email = isset( $entry['customer_email'] ) ? sanitize_email( $entry['customer_email'] ) : '';
			if ( $email ) {
				$customers[ hash_hmac( 'sha256', strtolower( $email ), wp_salt( 'auth' ) ) ] = true;
				continue;
			}
			$order_id = isset( $entry['order_id'] ) ? absint( $entry['order_id'] ) : 0;
			if ( $order_id ) {
				$customers[ 'order:' . $order_id ] = true;
			}
		}
		return count( $customers );
	}

	/**
	 * Handles the related plugin operation.
	 *
	 * @param mixed $limit Limit.
	 */
	public function get_recent_queue_items( $limit = 10 ) {
		$items = array_values( $this->get_queue() );
		usort(
			$items,
			function ( $a, $b ) {
				return absint( $b['updated_at'] ?? 0 ) - absint( $a['updated_at'] ?? 0 );
			}
		);
		$items = array_slice( $items, 0, absint( $limit ) );
		return $this->add_order_state_to_queue_items( $items );
	}

	/**
	 * Returns future customer-requested reminders for the admin Upcoming Customer Reminders panel.
	 *
	 * The primary source is the internal reminder list. A WooCommerce order-meta fallback is also
	 * used so a valid checkout request is still visible when the order meta was saved but the
	 * internal reminder row was not created during checkout. This keeps the admin list truthful
	 * and HPOS-safe by using WooCommerce order APIs only.
	 *
	 * @param int $limit Limit.
	 * @return array
	 */
	public function get_upcoming_customer_requested_items( $limit = 10 ) {
		$limit          = max( 1, absint( $limit ) );
		$items          = array();
		$seen_order_due = array();
		$now            = time();

		foreach ( $this->get_queue() as $entry ) {
			if ( ! is_array( $entry ) ) {
				continue;
			}

			$status = isset( $entry['status'] ) ? sanitize_key( $entry['status'] ) : 'pending';
			$source = isset( $entry['rule_source'] ) ? sanitize_key( $entry['rule_source'] ) : '';
			$due_at = isset( $entry['due_at'] ) ? absint( $entry['due_at'] ) : 0;
			$order_id = isset( $entry['order_id'] ) ? absint( $entry['order_id'] ) : 0;

			if ( 'pending' !== $status || 'checkout_request' !== $source || ! $due_at || $due_at < $now ) {
				continue;
			}

			if ( $order_id ) {
				$seen_order_due[ $order_id . '|' . $due_at ] = true;
			}

			$items[] = $entry;
		}

		foreach ( $this->get_checkout_request_order_items( $limit, $seen_order_due ) as $entry ) {
			$items[] = $entry;
		}

		usort(
			$items,
			function ( $a, $b ) {
				return absint( $a['due_at'] ?? 0 ) - absint( $b['due_at'] ?? 0 );
			}
		);

		$items = array_slice( $items, 0, $limit );
		return $this->add_order_state_to_queue_items( $items );
	}

	/**
	 * Builds admin reminder rows from WooCommerce orders that already store checkout reminder meta.
	 *
	 * @param int   $limit Limit.
	 * @param array $seen_order_due Existing order/date combinations.
	 * @return array
	 */
	private function get_checkout_request_order_items( $limit, $seen_order_due ) {
		if ( ! function_exists( 'wc_get_orders' ) ) {
			return array();
		}

		$limit          = max( 1, absint( $limit ) );
		$now            = time();
		$orders         = $this->get_checkout_request_orders( max( 25, $limit * 5 ) );
		$items          = array();
		$seen_order_due = is_array( $seen_order_due ) ? $seen_order_due : array();

		foreach ( $orders as $order ) {
			if ( ! $order || ! is_a( $order, 'WC_Order' ) ) {
				continue;
			}

			$order_id = absint( $order->get_id() );
			$due_at   = absint( $order->get_meta( self::META_CHECKOUT_REQUESTED_DUE_AT, true ) );

			if ( ! $order_id || ! $due_at || $due_at < $now || isset( $seen_order_due[ $order_id . '|' . $due_at ] ) ) {
				continue;
			}

			$item         = $this->get_first_reorderable_item( $order );
			$product      = $item && method_exists( $item, 'get_product' ) ? $item->get_product() : null;
			$item_id      = $item && method_exists( $item, 'get_id' ) ? absint( $item->get_id() ) : 0;
			$product_id   = $product && method_exists( $product, 'get_id' ) ? absint( $product->get_id() ) : 0;
			$product_name = $item && method_exists( $item, 'get_name' ) ? sanitize_text_field( $item->get_name() ) : '';

			if ( '' === $product_name ) {
				$product_name = sprintf( /* translators: %d: numeric order id. */ __( 'Order #%d reminder', 'noesia-commerce-suite' ), $order_id );
			}

			$items[] = array(
				'key'             => 'order-meta-' . $order_id . '-' . $due_at,
				'order_id'        => $order_id,
				'item_id'         => $item_id,
				'product_id'      => $product_id,
				'product_name'    => $product_name,
				'customer_email'  => sanitize_email( $order->get_billing_email() ),
				'due_at'          => $due_at,
				'reminder_days'   => 0,
				'rule_source'     => 'checkout_request',
				'status'          => 'pending',
				'created_at'      => absint( $order->get_meta( self::META_CHECKOUT_REQUESTED_AT, true ) ),
				'updated_at'      => time(),
				'last_error'      => '',
				'order_meta_fallback' => true,
			);

			$seen_order_due[ $order_id . '|' . $due_at ] = true;

			if ( count( $items ) >= $limit ) {
				break;
			}
		}

		return $items;
	}

	/**
	 * Gets WooCommerce orders that have checkout reminder request meta.
	 *
	 * @param int $limit Limit.
	 * @return array
	 */
	private function get_checkout_request_orders( $limit ) {
		$limit = max( 1, absint( $limit ) );
		$args  = array(
			'limit'      => $limit,
			'return'     => 'objects',
			'orderby'    => 'date',
			'order'      => 'DESC',
			'meta_query' => array(
				array(
					'key'     => self::META_CHECKOUT_REQUESTED_DUE_AT,
					'value'   => time(),
					'compare' => '>=',
					'type'    => 'NUMERIC',
				),
			),
		);

		if ( function_exists( 'wc_get_order_statuses' ) ) {
			$args['status'] = array_keys( wc_get_order_statuses() );
		}

		try {
			$orders = wc_get_orders( $args );
		} catch ( Exception $exception ) {
			$orders = array();
		}

		if ( ! empty( $orders ) ) {
			return is_array( $orders ) ? $orders : array();
		}

		unset( $args['meta_query'] );
		$args['limit'] = max( $limit, 50 );

		try {
			$orders = wc_get_orders( $args );
		} catch ( Exception $exception ) {
			$orders = array();
		}

		return is_array( $orders ) ? $orders : array();
	}

	/**
	 * Adds order opt-out state to reminder rows.
	 *
	 * @param array $items Items.
	 * @return array
	 */
	private function add_order_state_to_queue_items( $items ) {
		foreach ( $items as $index => $entry ) {
			$order_id  = is_array( $entry ) && isset( $entry['order_id'] ) ? absint( $entry['order_id'] ) : 0;
			$order     = $order_id && function_exists( 'wc_get_order' ) ? wc_get_order( $order_id ) : null;
			$items[ $index ]['opted_out']        = $order && $this->is_order_opted_out( $order );
			$items[ $index ]['opted_out_at']     = $order ? absint( $order->get_meta( self::META_OPT_OUT_AT, true ) ) : 0;
			$items[ $index ]['opted_out_source'] = $order ? sanitize_text_field( (string) $order->get_meta( self::META_OPT_OUT_SOURCE, true ) ) : '';
		}
		return $items;
	}

	/**
	 * Handles the related plugin operation.
	 *
	 * @param mixed $limit Limit.
	 */
	public function get_recent_opt_outs( $limit = 10 ) {
		$logs     = get_option( self::LOG_OPTION, array() );
		$logs     = is_array( $logs ) ? $logs : array();
		$records  = array();
		$seen     = array();

		foreach ( $logs as $log ) {
			if ( ! is_array( $log ) || 'opted_out' !== ( $log['status'] ?? '' ) ) {
				continue;
			}
			$order_id = isset( $log['order_id'] ) ? absint( $log['order_id'] ) : 0;
			if ( ! $order_id || isset( $seen[ $order_id ] ) ) {
				continue;
			}
			$order              = function_exists( 'wc_get_order' ) ? wc_get_order( $order_id ) : null;
			$source             = $order ? sanitize_text_field( (string) $order->get_meta( self::META_OPT_OUT_SOURCE, true ) ) : '';
			if ( ! in_array( $source, array( 'public link', 'public', 'email' ), true ) ) {
				continue;
			}
			$seen[ $order_id ]  = true;
			$name               = '';
			if ( $order && method_exists( $order, 'get_formatted_billing_full_name' ) ) {
				$name = trim( wp_strip_all_tags( $order->get_formatted_billing_full_name() ) );
			}
			$records[] = array(
				'order_id'  => $order_id,
				'label'     => $name ? $name : sprintf( /* translators: %d: numeric value. */ __( 'Order #%d customer', 'noesia-commerce-suite' ), $order_id ),
				'time'      => isset( $log['time'] ) ? absint( $log['time'] ) : 0,
				'source'    => $source,
				'message'   => sanitize_text_field( $log['message'] ?? '' ),
			);
			if ( count( $records ) >= absint( $limit ) ) {
				break;
			}
		}
		return $records;
	}

	/**
	 * Handles the related plugin operation.
	 *
	 * @param mixed $limit Limit.
	 */
	public function get_recent_logs( $limit = 10 ) {
		$logs  = get_option( self::LOG_OPTION, array() );
		$logs  = is_array( $logs ) ? $logs : array();
		return array_slice( $logs, 0, absint( $limit ) );
	}

	/**
	 * Handles the related plugin operation.
	 *
	 * @param mixed $queue Queue.
	 */
	private function save_queue( $queue ) {
		if ( count( $queue ) > 1000 ) {
			$queue = array_slice( $queue, -1000, null, true );
		}
		update_option( self::QUEUE_OPTION, $queue, false );
		delete_transient( 'noesia_cs_dashboard_stats_v1' );
		foreach ( array( 7, 30, 90, 365 ) as $noesia_cs_range ) {
			delete_transient( 'noesia_cs_dashboard_stats_v2_' . $noesia_cs_range );
		}
	}

	/**
	 * Handles the related plugin operation.
	 */
	private function get_sent_keys() {
		$sent = get_option( self::SENT_OPTION, array() );
		return is_array( $sent ) ? $sent : array();
	}

	/**
	 * Handles the related plugin operation.
	 *
	 * @param mixed $sent_keys Sent keys.
	 */
	private function save_sent_keys( $sent_keys ) {
		if ( count( $sent_keys ) > 2000 ) {
			$sent_keys = array_slice( $sent_keys, -2000, null, true );
		}
		update_option( self::SENT_OPTION, $sent_keys, false );
	}

	/**
	 * Handles the related plugin operation.
	 *
	 * @param mixed $status Status.
	 * @param mixed $order_id Order id.
	 * @param mixed $item_id Item id.
	 * @param mixed $message Message.
	 */
	private function add_log( $status, $order_id, $item_id, $message ) {
		$logs  = get_option( self::LOG_OPTION, array() );
		$logs  = is_array( $logs ) ? $logs : array();
		array_unshift(
			$logs,
			array(
				'time'      => time(),
				'status'    => sanitize_key( $status ),
				'order_id'  => absint( $order_id ),
				'item_id'   => absint( $item_id ),
				'message'   => sanitize_text_field( $message ),
			)
		);
		$logs = array_slice( $logs, 0, 300 );
		update_option( self::LOG_OPTION, $logs, false );
	}

	/**
	 * Handles the related plugin operation.
	 *
	 * @param mixed $order_id Order id.
	 * @param mixed $item_id Item id.
	 * @param mixed $product_id Product id.
	 * @param mixed $due_at Due at.
	 */
	private function build_key( $order_id, $item_id, $product_id, $due_at ) {
		return sha1( absint( $order_id ) . '|' . absint( $item_id ) . '|' . absint( $product_id ) . '|' . absint( $due_at ) );
	}
}
