<?php

namespace AutomateWoo;

use AutomateWoo\Exceptions\InvalidArgument;
use AutomateWoo\Triggers\AbstractBatchedDailyTrigger;
use RuntimeException;
use WC_Payment_Tokens;

defined( 'ABSPATH' ) || exit;

/**
 * Trigger_Customer_Before_Saved_Card_Expiry class.
 *
 * @since 3.7.0
 */
class Trigger_Customer_Before_Saved_Card_Expiry extends AbstractBatchedDailyTrigger {

	/**
	 * @var string[]
	 */
	public $supplied_data_items = [ 'customer', 'card' ];

	/**
	 * Load admin details.
	 */
	public function load_admin_details() {
		$this->title       = __( 'Customer Before Saved Card Expiry', 'automatewoo' );
		$this->description = sprintf(
			/* translators: %1$s docs URL */
			__(
				'This trigger runs a set number of days before a customer\'s saved card expires. Cards expire on the last calendar day of their expiry month. <a href="%1$s" target="_blank">Some payment gateways are not supported.</a>',
				'automatewoo'
			),
			Admin::get_docs_link( 'triggers/saved-card-expiry-notifications/' )
		);
		$this->description .= ' ' . $this->get_description_text_workflow_not_immediate();
		$this->group        = __( 'Customers', 'automatewoo' );
	}

	/**
	 * Load fields.
	 */
	public function load_fields() {
		$days_before = ( new Fields\Positive_Number() )
			->set_name( 'days_before_expiry' )
			->set_title( __( 'Days before expiry', 'automatewoo' ) )
			->set_required();

		$this->add_field( $days_before );
		$this->add_field( $this->get_field_time_of_day() );
	}


	/**
	 * Get credit cards based on the specified days before expiry field.
	 *
	 * Uses cursor-based pagination (token_id > $after_id) to prevent skipping items
	 * when the dataset changes between async batch runs.
	 *
	 * @since 6.2.3 Updated to use cursor-based pagination.
	 *
	 * @param Workflow $workflow
	 * @param int      $limit
	 * @param int      $after_id Return tokens with ID greater than this value.
	 *
	 * @return array
	 */
	protected function get_cards_by_expiry( $workflow, $limit, $after_id ) {
		global $wpdb;

		$days_before = absint( $workflow->get_trigger_option( 'days_before_expiry' ) );

		if ( ! $days_before ) {
			return [];
		}

		// Anchor on the date this batch was scheduled to run for (falls back to now)
		// so the target expiry month isn't shifted if the batch runs after midnight.
		$date = $this->get_batch_base_date();
		$date->convert_to_site_time(); // get cards based on the sites timezone
		$date->modify( "{$days_before} days" );

		$day_to_run    = (int) $date->format( 'j' );
		$days_in_month = (int) $date->format( 't' );

		if ( $days_in_month !== $day_to_run ) {
			return [];
		}

		$results = $wpdb->get_results(
			$wpdb->prepare(
				"SELECT token_id FROM {$wpdb->prefix}woocommerce_payment_tokens as tokens
				LEFT JOIN {$wpdb->payment_tokenmeta} AS m1 ON tokens.token_id = m1.payment_token_id
				LEFT JOIN {$wpdb->payment_tokenmeta} AS m2 ON tokens.token_id = m2.payment_token_id
				WHERE type = 'CC'
				AND m1.meta_key = 'expiry_year'
				AND m1.meta_value = %s
				AND m2.meta_key = 'expiry_month'
				AND m2.meta_value = %s
				AND tokens.token_id > %d
				ORDER BY tokens.token_id ASC
				LIMIT %d
				",
				[
					$date->format( 'Y' ),
					$date->format( 'm' ),
					$after_id,
					$limit,
				]
			),
			OBJECT_K
		);

		return array_keys( $results );
	}

	/**
	 * 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.
	 */
	public function get_batch_for_workflow( Workflow $workflow, int $after_id, int $limit ): array {
		$items = [];

		foreach ( $this->get_cards_by_expiry( $workflow, $limit, $after_id ) as $token_id ) {
			$items[] = [
				'token' => $token_id,
			];
		}

		return $items;
	}

	/**
	 * Process a single item for a workflow to process.
	 *
	 * @param Workflow $workflow
	 * @param array    $item
	 *
	 * @throws InvalidArgument If token is not set.
	 * @throws RuntimeException If token is not found.
	 */
	public function process_item_for_workflow( Workflow $workflow, array $item ) {
		if ( ! isset( $item['token'] ) ) {
			throw InvalidArgument::missing_required( 'token' );
		}

		$token = WC_Payment_Tokens::get( $item['token'] );
		if ( ! $token ) {
			throw new RuntimeException( 'Token was not found.' );
		}

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

	/**
	 * @param Workflow $workflow
	 *
	 * @return bool
	 */
	public function validate_workflow( $workflow ) {
		// workflow should only run once for each card
		if ( $workflow->has_run_for_data_item( 'card' ) ) {
			return false;
		}

		return true;
	}
}
