<?php

namespace AutomateWoo;

use AutomateWoo\Triggers\TriggerInterface;
use AutomateWoo\Workflows\Factory;

/**
 * @class Trigger
 */
abstract class Trigger implements TriggerInterface {

	/** @var string */
	public $title;

	/** @var string */
	public $name;

	/** @var string */
	public $description;

	/** @var string */
	public $group;

	/** @var array */
	public $supplied_data_items = [];

	/** @var array */
	public $fields = [];

	/** @var array */
	public $options;

	/** @var array */
	protected $rules;

	/** @var bool */
	protected $has_loaded_fields = false;

	/** @var bool */
	public $has_loaded_admin_details = false;

	/** @var array */
	protected $limit_trigger_to_specific_workflows = [];

	/**
	 * Async events required by the trigger.
	 *
	 * @since 4.8.0
	 * @var array|string
	 */
	protected $required_async_events;

	/**
	 * Define whether workflows using this trigger can be queued. Defaults to true
	 *
	 * @since 3.8
	 */
	const SUPPORTS_QUEUING = true;

	/**
	 * Define if workflows using this trigger are run daily at a specific time. Defaults to false.
	 *
	 * @since 3.8
	 */
	const SUPPORTS_CUSTOM_TIME_OF_DAY = false;


	/**
	 * Register the hooks the trigger listens to.
	 *
	 * @return void
	 */
	abstract public function register_hooks();


	/**
	 * Construct
	 */
	public function __construct() {
		$this->init();
		$this->supplied_data_items[] = 'shop';

		// compatibility for user and customer objects
		if ( in_array( 'user', $this->supplied_data_items, true ) ) {
			$this->supplied_data_items[] = 'customer';
		}

		// backwards compat for custom triggers using the user data item, IMPORTANT to exclude guest triggers
		if ( in_array( 'customer', $this->supplied_data_items, true ) && ! in_array( 'guest', $this->supplied_data_items, true ) ) {
			$this->supplied_data_items[] = 'user';
		}

		$this->supplied_data_items = array_unique( $this->supplied_data_items );

		add_action( 'automatewoo_init_triggers', [ $this, 'register_hooks' ] );
	}


	/**
	 * Init
	 */
	public function init() {}


	/**
	 * Method to set title, group, description and other admin props
	 */
	public function load_admin_details() {}


	/**
	 * Registers any fields used on for a trigger
	 */
	public function load_fields() {}


	/**
	 * Admin info loader
	 */
	public function maybe_load_admin_details() {
		if ( ! $this->has_loaded_admin_details ) {
			$this->load_admin_details();
			$this->has_loaded_admin_details = true;
		}
	}


	/**
	 * Field loader
	 */
	public function maybe_load_fields() {
		if ( ! $this->has_loaded_fields ) {
			$this->load_fields();
			$this->has_loaded_fields = true;
		}
	}


	/**
	 * @param \AutomateWoo\Workflow $workflow
	 * @return bool
	 */
	public function validate_workflow( $workflow ) {
		return true;
	}

	/**
	 * @param object $option
	 */
	public function add_field( $option ) {
		$option->set_name_base( 'aw_workflow_data[trigger_options]' );
		$this->fields[ $option->get_name() ] = $option;
	}


	/**
	 * @param string $option_name
	 */
	public function remove_field( $option_name ) {
		if ( isset( $this->fields[ $option_name ] ) ) {
			unset( $this->fields[ $option_name ] );
		}
	}


	/**
	 * @return array
	 */
	public function get_supplied_data_items() {
		return $this->supplied_data_items;
	}


	/**
	 * @param string $name
	 *
	 * @return Fields\Field|false
	 */
	public function get_field( $name ) {
		$this->maybe_load_fields();

		if ( ! isset( $this->fields[ $name ] ) ) {
			return false;
		}

		return $this->fields[ $name ];
	}


	/**
	 * @return Fields\Field[]
	 */
	public function get_fields() {
		$this->maybe_load_fields();
		return $this->fields;
	}

	/**
	 * Get a list of required field names.
	 *
	 * @since 6.0.10
	 *
	 * @return Field[]
	 */
	public function get_required_fields(): array {
		$required_fields = [];
		foreach ( $this->get_fields() as $name => $field ) {
			if ( $field->get_required() ) {
				$required_fields[ $name ] = $field;
			}
		}

		return $required_fields;
	}

	/**
	 * @return bool
	 */
	public function has_workflows() {
		$query = new \WP_Query(
			[
				'post_type'        => 'aw_workflow',
				'post_status'      => 'publish',
				'fields'           => 'ids',
				'posts_per_page'   => 1,
				'meta_query'       => [
					[
						'key'   => 'trigger_name',
						'value' => $this->get_name(),
					],
				],
				'suppress_filters' => true,
				'no_found_rows'    => true,
			]
		);

		return $query->post_count !== 0;
	}


	/**
	 * @return array
	 */
	public function get_workflow_ids() {
		if ( $this->limit_trigger_to_specific_workflows ) {
			// workflow ids have been explicitly set
			return $this->limit_trigger_to_specific_workflows;
		}

		// otherwise get all workflows that using this trigger, including other translations
		$query = new Workflow_Query();
		$query->set_return( 'ids' );
		$query->set_trigger( $this->get_name() );
		$workflows = $query->get_results();

		return $workflows;
	}


	/**
	 * @return Workflow[]
	 */
	public function get_workflows() {
		$workflows = [];

		foreach ( $this->get_workflow_ids() as $workflow_id ) {
			$workflow = Factory::get( $workflow_id );
			if ( $workflow ) {
				$workflows[] = $workflow;
			}
		}

		return apply_filters( 'automatewoo/trigger/workflows', $workflows, $this );
	}


	/**
	 * Every data item registered with the trigger should be supplied to this method in its object form.
	 * E.g. a 'user' should be passed as a WP_User object, and an 'order' should be passed as a WC_Order object
	 *
	 * @param Data_Layer|array $data_layer
	 */
	public function maybe_run( $data_layer = [] ) {

		// Get all workflows that are registered to use this trigger
		$workflows = $this->get_workflows();
		if ( ! $workflows ) {
			return;
		}

		// Check if each workflow should be run based on its options
		foreach ( $workflows as $workflow ) {
			$workflow->maybe_run( $data_layer );
		}
	}


	/**
	 * @return string
	 */
	public function get_name() {
		return $this->name;
	}


	/**
	 * @param string $name
	 */
	public function set_name( $name ) {
		$this->name = $name;
	}


	/**
	 * @return string
	 */
	public function get_title() {
		$this->maybe_load_admin_details();
		return $this->title;
	}


	/**
	 * @return string
	 */
	public function get_group() {
		$this->maybe_load_admin_details();
		return $this->group ? $this->group : __( 'Other', 'automatewoo' );
	}


	/**
	 * @return string|null
	 */
	public function get_description() {
		$this->maybe_load_admin_details();
		return $this->description;
	}


	/**
	 * @return string
	 */
	public function get_description_html() {

		if ( ! $this->get_description() ) {
			return '';
		}

		return '<p class="aw-field-description">' . $this->get_description() . '</p>';
	}


	/**
	 * @param array $options
	 * @deprecated
	 */
	public function set_options( $options ) {
		$this->options = $options;
	}


	/**
	 * Will return all data if $field is false
	 *
	 * @param string $field
	 * @return mixed
	 *
	 * @deprecated use $workflow->get_trigger_option()
	 */
	public function get_option( $field ) {

		if ( ! $field ) {
			return false;
		}

		$value = false;

		if ( isset( $this->options[ $field ] ) ) {
			$value = $this->options[ $field ];
		}

		return apply_filters( 'automatewoo_trigger_option', $value, $field, $this );
	}



	/**
	 * This method is called just before a queued workflow runs
	 *
	 * @param Workflow $workflow
	 * @return bool
	 */
	public function validate_before_queued_event( $workflow ) {
		return true;
	}


	/**
	 * Checks if this trigger's language matches that of the user or guest
	 *
	 * @param Workflow $workflow
	 * @return bool
	 */
	public function validate_workflow_language( $workflow ) {

		if ( ! Language::is_multilingual() ) {
			return true;
		}

		$workflow_lang = $workflow->get_language();
		if ( ! $workflow_lang ) {
			return true; // workflow has no set language
		}

		$data_lang = $workflow->data_layer()->get_customer_language();
		if ( ! $data_lang ) {
			return true;
		}

		return $data_lang === $workflow_lang;
	}


	/**
	 * @since 4.2.0
	 * @param array|int $workflow_ids
	 */
	public function limit_trigger_to_specific_workflows( $workflow_ids ) {
		$this->limit_trigger_to_specific_workflows = (array) $workflow_ids;
	}


	/**
	 * @since 4.2.0
	 * Removes trigger limitation and allows triggering for any workflow
	 */
	public function remove_limit_trigger_to_specific_workflows() {
		$this->limit_trigger_to_specific_workflows = [];
	}



	/**
	 * Add the "recheck status before run" checkbox field for queued order status.
	 *
	 * @return void
	 */
	protected function add_field_validate_queued_order_status() {

		$field = new Fields\Checkbox();
		$field->set_name( 'validate_order_status_before_queued_run' );
		$field->set_title( __( 'Recheck status before run', 'automatewoo' ) );
		$field->default_to_checked = true;
		$field->set_description(
			__(
				"This is useful for Workflows that are not run immediately as it ensures the status hasn't changed since initial trigger.",
				'automatewoo'
			)
		);

		$this->add_field( $field );
	}


	/**
	 * Add the customer pause period field.
	 *
	 * @return void
	 */
	protected function add_field_user_pause_period() {

		$field = ( new Fields\Positive_Number() )
			->set_name( 'user_pause_period' )
			->set_title( __( 'Customer pause period (days)', 'automatewoo' ) )
			->set_description( __( 'Can be used to ensure that this trigger will only send once in a set period to a user or guest.', 'automatewoo' ) );
		$this->add_field( $field );
	}


	/**
	 * @param string $object_name
	 */
	protected function add_field_recheck_status( $object_name ) {

		$field = ( new Fields\Checkbox() )
			->set_name( 'recheck_status_before_queued_run' )
			->set_title( __( 'Recheck status before run', 'automatewoo' ) )
			->set_default_to_checked()
			->set_description(
				sprintf(
					/* translators: %s Object name. */
					__( "This is useful for workflows that are not run immediately as it ensures the status of the %s hasn't changed since initial trigger.", 'automatewoo' ),
					$object_name
				)
			);

		$this->add_field( $field );
	}


	/**
	 * Order status field must be named 'order_status'
	 *
	 * @param Trigger   $trigger
	 * @param \WC_Order $order
	 * @deprecated
	 * @return bool
	 * @since 2.0
	 */
	protected function validate_order_status_field( $trigger, $order ) {

		wc_deprecated_function( __METHOD__, '5.2.0' );

		$status = Clean::string( $trigger->get_option( 'order_status' ) );

		if ( ! $status ) {
			return true;
		}

		$status = 'wc-' === substr( $status, 0, 3 ) ? substr( $status, 3 ) : $status;

		// wrong status
		if ( $order->get_status() !== $status ) {
			return false;
		}

		return true;
	}



	/**
	 * @param Workflow $workflow
	 * @return bool
	 */
	protected function validate_field_user_pause_period( $workflow ) {

		$period = $workflow->get_trigger_option( 'user_pause_period' );

		$customer = $workflow->data_layer()->get_customer();
		$user     = $workflow->data_layer()->get_user();
		$guest    = $workflow->data_layer()->get_guest();

		// There could be no pause period set, and the value of $period will be false.
		// In addition, users had been allowed to save a non-positive int, and these invalid values
		// were run with the same result as the default value. Because there are no logs recorded
		// with a future timestamp when querying, misleadingly made the validation result false.
		//
		// Here it returns invalid values earlier, following consistent results and avoiding
		// further log queries with invalid values.
		if ( ! $period || $period <= 0 ) {
			return true;
		}

		if ( ! $user && ! $guest && ! $customer ) {
			return true; // must have a customer, user or guest
		}

		$days = -1 * (int) $period;

		$period_date = new DateTime();
		$period_date->modify( "{$days} days" );

		// Check to see if this workflow has run since the period date
		$log_query = new Log_Query();
		$log_query->where_workflow( $workflow->get_translation_ids() );
		$log_query->where_date( $period_date, '>' );

		if ( $customer ) {
			$log_query->where_customer( $customer->get_id() );
		} elseif ( $user ) {
			if ( $user->ID === 0 ) { // guest user
				$log_query->where_guest( $user->user_email );
			} else {
				$log_query->where_user( $user->ID );
			}
		} elseif ( $guest ) {
			$log_query->where_guest( $guest->get_email() );
		}

		if ( $log_query->has_results() ) {
			return false;
		}

		return true;
	}


	/**
	 * @param array|string $allowed_statuses
	 * @param string       $current_status
	 *
	 * @return bool
	 */
	protected function validate_status_field( $allowed_statuses, $current_status ) {
		// allow all if left blank
		if ( empty( $allowed_statuses ) ) {
			return true;
		}

		if ( is_array( $allowed_statuses ) ) {
			// multi status match
			$with_prefix_match = in_array( 'wc-' . $current_status, $allowed_statuses, true );
			$no_prefix_match   = in_array( $current_status, $allowed_statuses, true );

			// at least one has to match
			if ( ! $with_prefix_match && ! $no_prefix_match ) {
				return false;
			}
		} else {
			// single status match, remove prefix
			$allowed_statuses = 'wc-' === substr( $allowed_statuses, 0, 3 ) ? substr( $allowed_statuses, 3 ) : $allowed_statuses;

			if ( $allowed_statuses !== $current_status ) {
				return false;
			}
		}

		return true;
	}


	/**
	 * Get the order status change hook, async or instant
	 *
	 * @return string
	 */
	protected function get_hook_order_status_changed() {
		return AUTOMATEWOO_DISABLE_ASYNC_ORDER_STATUS_CHANGED ? 'woocommerce_order_status_changed' : 'automatewoo/order/status_changed_async';
	}


	/**
	 * Get the subscription status change hook, async or instant
	 *
	 * @return string
	 */
	protected function get_hook_subscription_status_changed() {
		return AUTOMATEWOO_DISABLE_ASYNC_SUBSCRIPTION_STATUS_CHANGED ? 'automatewoo/subscription/status_changed' : 'automatewoo/subscription/status_changed_async';
	}


	/**
	 * @return string
	 */
	protected function get_deprecation_warning() {
		return __( 'THIS TRIGGER IS DEPRECATED AND SHOULD NOT BE USED.', 'automatewoo' );
	}

	/**
	 * Get the trigger's required async events.
	 *
	 * @since 4.8.0
	 *
	 * @return array
	 */
	public function get_required_async_events() {
		return (array) $this->required_async_events;
	}

	/**
	 * Set the trigger's required async events.
	 *
	 * @since 4.8.0
	 *
	 * @param array|string $required_async_events
	 */
	public function set_required_async_events( $required_async_events ) {
		$this->required_async_events = $required_async_events;
	}





	/**
	 * Get the order paid, async or instant
	 *
	 * @deprecated because automatewoo/order/paid is now also async due to #158
	 *
	 * @return string
	 */
	protected function get_hook_order_paid() {
		wc_deprecated_function( __METHOD__, '5.2.0' );

		return 'automatewoo/order/paid_async';
	}

	/**
	 * Used to dynamically load option values for a trigger field.
	 *
	 * @param string       $field_name
	 * @param string|false $reference_field_value
	 *
	 * @return array
	 * @since 5.6.6
	 */
	public function get_dynamic_field_options( $field_name, $reference_field_value = false ) {
		return [];
	}
}
