<?php

namespace AutomateWoo;

use AutomateWoo\Exceptions\InvalidArgument;
use AutomateWoo\Traits\IntegerValidator;
use AutomateWoo\Triggers\AbstractBatchedDailyTrigger;
use WP_Query;

defined( 'ABSPATH' ) || exit;

/**
 * @class Trigger_Subscription_Before_Renewal
 * @since 2.6.2
 */
class Trigger_Subscription_Before_Renewal extends AbstractBatchedDailyTrigger {

	use IntegerValidator;

	/**
	 * Sets supplied data for the trigger.
	 *
	 * @var array
	 */
	public $supplied_data_items = [ 'customer', 'subscription' ];

	/**
	 * Method to set the action's admin props.
	 *
	 * Admin props include: title, group and description.
	 */
	public function load_admin_details() {
		$this->title        = __( 'Subscription Before Renewal', 'automatewoo' );
		$this->description  = __( "This trigger runs once per day for any subscriptions that are due for renewal on the workflow's target date. For example, if set to run 7 days before renewal, it would look for subscriptions that are due for renewal on the date exactly 7 days from now.", 'automatewoo' );
		$this->description .= ' ' . $this->get_description_text_workflow_not_immediate();
		$this->group        = Subscription_Workflow_Helper::get_group_name();
	}

	/**
	 * Load fields.
	 */
	public function load_fields() {

		$days_before_renewal = ( new Fields\Positive_Number() )
			->set_name( 'days_before_renewal' )
			->set_title( __( 'Days before renewal', 'automatewoo' ) )
			->set_required();

		$this->add_field( $days_before_renewal );
		$this->add_field( $this->get_field_time_of_day() );
		$this->add_field( Subscription_Workflow_Helper::get_products_field() );
	}

	/**
	 * Get a batch of items to process for given workflow.
	 *
	 * @param Workflow $workflow
	 * @param int      $after_id Items with ID greater than this value (cursor for pagination).
	 * @param int      $limit    The max items for the query.
	 *
	 * @return array[] Array of items in array format. Items will be stored in the database so they should be IDs not objects.
	 *
	 * @throws InvalidArgument If workflow 'days before' option is not valid.
	 */
	public function get_batch_for_workflow( Workflow $workflow, int $after_id, int $limit ): array {
		$items = [];

		foreach ( $this->get_subscriptions_for_workflow( $workflow, $after_id, $limit ) as $subscription_id ) {
			$items[] = [
				'subscription' => $subscription_id,
			];
		}

		return $items;
	}

	/**
	 * Process a single item for a workflow to process.
	 *
	 * @param Workflow $workflow
	 * @param array    $item
	 */
	public function process_item_for_workflow( Workflow $workflow, array $item ) {
		$subscription = isset( $item['subscription'] ) ? wcs_get_subscription( Clean::id( $item['subscription'] ) ) : false;
		if ( ! $subscription ) {
			return;
		}

		$workflow->maybe_run(
			[
				'subscription' => $subscription,
				'customer'     => Customer_Factory::get_by_user_id( $subscription->get_user_id() ),
			]
		);
	}

	/**
	 * Get subscriptions that match the workflow's date params.
	 *
	 * @param Workflow $workflow
	 * @param int      $after_id
	 * @param int      $limit
	 *
	 * @return int[] Array of subscription IDs.
	 *
	 * @throws InvalidArgument If workflow 'days before' option is not valid.
	 */
	protected function get_subscriptions_for_workflow( Workflow $workflow, int $after_id, int $limit ) {
		$days_before_renewal = (int) $workflow->get_trigger_option( 'days_before_renewal' );
		$this->validate_positive_integer( $days_before_renewal );

		$date = $this->get_batch_base_date()->add( new \DateInterval( "P{$days_before_renewal}D" ) );

		return $this->query_subscriptions_for_day( $date, '_schedule_next_payment', [ 'wc-active' ], $after_id, $limit );
	}

	/**
	 * Query subscriptions for a specific day.
	 *
	 * Uses cursor-based pagination (ID > $after_id) instead of offset to prevent
	 * skipping items when the dataset changes between async batch runs.
	 *
	 * @since 6.2.3 Updated to use cursor-based pagination.
	 *
	 * @param DateTime $date          The target date in UTC timezone.
	 * @param string   $date_meta_key The subscription date meta key to query.
	 * @param array    $statuses      The subscription statuses to query.
	 * @param int      $after_id      Return subscriptions with ID greater than this value.
	 * @param int      $limit
	 *
	 * @return int[] Array of subscription IDs.
	 */
	protected function query_subscriptions_for_day( DateTime $date, string $date_meta_key, array $statuses, int $after_id, int $limit ) {
		$date->convert_to_site_time();
		$day_start = clone $date;
		$day_end   = clone $date;
		$day_start->set_time_to_day_start();
		$day_end->set_time_to_day_end();
		$day_start->convert_to_utc_time();
		$day_end->convert_to_utc_time();

		if ( function_exists( 'wcs_get_orders_with_meta_query' ) ) {
			$args = [
				'type'          => 'shop_subscription',
				'status'        => $statuses,
				'return'        => 'ids',
				'limit'         => $limit,
				'orderby'       => 'ID',
				'order'         => 'ASC',
				'no_found_rows' => true,
				'meta_query'    => [
					[
						'key'     => $date_meta_key,
						'compare' => 'BETWEEN',
						'value'   => [ $day_start->to_mysql_string(), $day_end->to_mysql_string() ],
					],
				],
			];

			$where_filter = null;

			if ( $after_id > 0 ) {
				if ( function_exists( 'wcs_is_custom_order_tables_usage_enabled' ) && wcs_is_custom_order_tables_usage_enabled() ) {
					$args['field_query'] = [
						[
							'field'   => 'id',
							'value'   => $after_id,
							'compare' => '>',
						],
					];
				} else {
					$where_filter = function ( $where ) use ( $after_id ) {
						global $wpdb;
						$where .= $wpdb->prepare( " AND {$wpdb->posts}.ID > %d", $after_id );
						return $where;
					};
					add_filter( 'posts_where', $where_filter );
				}
			}

			try {
				return wcs_get_orders_with_meta_query( $args );
			} finally {
				if ( $where_filter ) {
					remove_filter( 'posts_where', $where_filter );
				}
			}
		}

		// Fallback for querying subscriptions before HPOS compatibility was added.
		$query_args = [
			'post_type'      => 'shop_subscription',
			'post_status'    => $statuses,
			'fields'         => 'ids',
			'posts_per_page' => $limit,
			'orderby'        => 'ID',
			'order'          => 'ASC',
			'no_found_rows'  => true,
			'meta_query'     => [
				[
					'key'     => $date_meta_key,
					'compare' => '>=',
					'value'   => $day_start->to_mysql_string(),
				],
				[
					'key'     => $date_meta_key,
					'compare' => '<=',
					'value'   => $day_end->to_mysql_string(),
				],
			],
		];

		$where_filter = null;

		if ( $after_id > 0 ) {
			$where_filter = function ( $where ) use ( $after_id ) {
				global $wpdb;
				$where .= $wpdb->prepare( " AND {$wpdb->posts}.ID > %d", $after_id );
				return $where;
			};
			add_filter( 'posts_where', $where_filter );
		}

		try {
			$query = new WP_Query( $query_args );
			return $query->posts;
		} finally {
			if ( $where_filter ) {
				remove_filter( 'posts_where', $where_filter );
			}
		}
	}


	/**
	 * Handle workflow validation.
	 *
	 * @param Workflow $workflow
	 * @return bool
	 */
	public function validate_workflow( $workflow ) {

		$subscription = $workflow->data_layer()->get_subscription();

		if ( ! $subscription ) {
			return false;
		}

		if ( ! Subscription_Workflow_Helper::validate_products_field( $workflow ) ) {
			return false;
		}

		// ensure that the workflow has not triggered for this subscription in the last 24  hours
		// this avoids duplication that could arise from timezone/DST changes
		if ( $workflow->has_run_for_data_item( 'subscription', DAY_IN_SECONDS ) ) {
			return false;
		}

		return true;
	}


	/**
	 * Validate before a queued workflow event.
	 *
	 * @param Workflow $workflow
	 * @return bool
	 */
	public function validate_before_queued_event( $workflow ) {
		$subscription = $workflow->data_layer()->get_subscription();

		if ( ! $subscription ) {
			return false;
		}

		// only trigger for active subscriptions
		if ( ! $subscription->has_status( 'active' ) ) {
			return false;
		}

		return true;
	}
}
