<?php

namespace AutomateWoo;

use AutomateWoo\DataTypes\DataTypes;
use AutomateWoo\Workflows\Factory;

defined( 'ABSPATH' ) || exit;

/**
 * @class Queued_Event
 *
 * @property array $data_items (legacy)
 */
class Queued_Event extends Abstract_Model_With_Meta_Table {

	const CUSTOMER_ID_META_KEY = 'customer_id';
	const GUEST_ID_META_KEY    = 'guest_id';

	/** @var string */
	public $table_id = 'queue';

	/** @var string  */
	public $object_type = 'queue';

	/** @var bool|array */
	private $uncompressed_data_layer;


	// error messages
	const F_WORKFLOW_INACTIVE = 100;
	const F_MISSING_DATA      = 101;
	const F_FATAL_ERROR       = 102;

	/**
	 * Returns the ID of the model's meta table.
	 *
	 * @return string
	 */
	public function get_meta_table_id() {
		return 'queue-meta';
	}

	/**
	 * @param bool|int $id
	 */
	public function __construct( $id = false ) {
		if ( $id ) {
			$this->get_by( 'id', $id );
		}
	}


	/**
	 * Set workflow ID prop.
	 *
	 * @param int $id
	 *
	 * @return $this
	 */
	public function set_workflow_id( $id ) {
		$this->set_prop( 'workflow_id', Clean::id( $id ) );
		return $this;
	}


	/**
	 * @return int
	 */
	public function get_workflow_id() {
		return Clean::id( $this->get_prop( 'workflow_id' ) );
	}


	/**
	 * Set failed prop.
	 *
	 * @param bool $failed
	 *
	 * @return $this
	 */
	public function set_failed( $failed = true ) {
		$this->set_prop( 'failed', aw_bool_int( $failed ) );
		return $this;
	}


	/**
	 * @return bool
	 */
	public function is_failed() {
		return (bool) $this->get_prop( 'failed' );
	}


	/**
	 * @param int $failure_code
	 *
	 * @return $this
	 */
	public function set_failure_code( $failure_code ) {
		$this->set_prop( 'failure_code', absint( $failure_code ) );
		return $this;
	}

	/**
	 * @return int
	 */
	public function get_failure_code() {
		return absint( $this->get_prop( 'failure_code' ) );
	}


	/**
	 * @param DateTime $date
	 *
	 * @return $this
	 */
	public function set_date_created( $date ) {
		$this->set_date_column( 'created', $date );
		return $this;
	}


	/**
	 * @return bool|DateTime
	 */
	public function get_date_created() {
		return $this->get_date_column( 'created' );
	}


	/**
	 * @param DateTime $date
	 *
	 * @return $this
	 */
	public function set_date_due( $date ) {
		$this->set_date_column( 'date', $date );
		return $this;
	}


	/**
	 * @return bool|DateTime
	 */
	public function get_date_due() {
		return $this->get_date_column( 'date' );
	}


	/**
	 * @param Data_Layer $data_layer The data layer for the queued event
	 */
	public function store_data_layer( $data_layer ) {
		$this->uncompressed_data_layer = $data_layer->get_raw_data();

		foreach ( $this->uncompressed_data_layer as $data_type_id => $data_item ) {
			$this->store_data_item( $data_type_id, $data_item );
		}

		$this->store_customer_ids( $data_layer );
	}

	/**
	 * Store customer identifiers for lightweight admin queue display.
	 *
	 * @param Data_Layer $data_layer The data layer for the queued event.
	 */
	private function store_customer_ids( $data_layer ) {
		$data     = $data_layer->get_raw_data();
		$customer = isset( $data['customer'] ) ? aw_validate_data_item( 'customer', $data['customer'] ) : false;

		if ( $customer ) {
			$this->update_meta( self::CUSTOMER_ID_META_KEY, $customer->get_id() );

			if ( $customer->get_guest_id() ) {
				$this->update_meta( self::GUEST_ID_META_KEY, $customer->get_guest_id() );
			}

			return;
		}

		$guest = isset( $data['guest'] ) ? aw_validate_data_item( 'guest', $data['guest'] ) : false;
		if ( $guest && $guest->get_id() ) {
			$this->update_meta( self::GUEST_ID_META_KEY, $guest->get_id() );
			return;
		}

		$customer = $this->get_customer_from_data_layer( $data_layer );
		if ( $customer ) {
			$this->update_meta( self::CUSTOMER_ID_META_KEY, $customer->get_id() );

			if ( $customer->get_guest_id() ) {
				$this->update_meta( self::GUEST_ID_META_KEY, $customer->get_guest_id() );
			}
		}
	}

	/**
	 * Resolve a customer from the available data layer items.
	 *
	 * @param Data_Layer $data_layer The data layer for the queued event.
	 *
	 * @return Customer|false
	 */
	private function get_customer_from_data_layer( $data_layer ) {
		$user = $data_layer->get_user();
		if ( $user ) {
			return Customer_Factory::get_by_user_data_item( $user );
		}

		$order = $data_layer->get_order();
		if ( $order ) {
			return Customer_Factory::get_by_order( $order );
		}

		$subscription = $data_layer->get_subscription();
		if ( $subscription ) {
			return Customer_Factory::get_by_user_id( $subscription->get_user_id() );
		}

		return false;
	}

	/**
	 * Get the stored customer ID for this queued event.
	 *
	 * @return int
	 */
	public function get_customer_id() {
		return absint( $this->get_meta( self::CUSTOMER_ID_META_KEY ) );
	}

	/**
	 * Get the stored guest ID for this queued event.
	 *
	 * @return int
	 */
	public function get_guest_id() {
		return absint( $this->get_meta( self::GUEST_ID_META_KEY ) );
	}

	/**
	 * Get the customer from lightweight stored queue metadata.
	 *
	 * @return Customer|false
	 */
	public function get_stored_customer() {
		$customer_id = $this->get_customer_id();
		if ( $customer_id ) {
			$customer = Customer_Factory::get( $customer_id );
			if ( $customer ) {
				return $customer;
			}
		}

		$guest_id = $this->get_guest_id();
		if ( $guest_id ) {
			return Customer_Factory::get_by_guest_id( $guest_id );
		}

		return false;
	}

	/**
	 * @param string $data_type_id Data type object ID
	 * @param mixed  $data_item    The data item
	 */
	private function store_data_item( $data_type_id, $data_item ) {
		$data_type = DataTypes::get( $data_type_id );

		if (
			! $data_type ||
			! $data_type->validate( $data_item ) ||
			DataTypes::is_non_stored_data_type( $data_type_id )
		) {
			return;
		}

		$storage_key   = Queue_Manager::get_data_layer_storage_key( $data_type_id );
		$storage_value = $data_type->compress( $data_item );

		if ( $storage_key ) {
			$this->update_meta( $storage_key, $storage_value );
		}
	}


	/**
	 * @return Data_Layer The data layer instance
	 */
	public function get_data_layer() {
		if ( ! isset( $this->uncompressed_data_layer ) ) {

			$uncompressed_data_layer = [];
			$compressed_data_layer   = $this->get_compressed_data_layer();

			if ( $compressed_data_layer ) {
				foreach ( $compressed_data_layer as $data_type_id => $compressed_item ) {
					$data_type = DataTypes::get( $data_type_id );
					if ( $data_type ) {
						$decompressed_data = $data_type->decompress( $compressed_item, $compressed_data_layer );
						if ( ! $decompressed_data && $this->id ) {
							$message = sprintf(
								'Some of the required data was not found. Queued event ID: %s. The missing "%s" data\'s ID: %d.',
								$this->id,
								$data_type_id,
								Clean::id( $compressed_item )
							);
							if ( 'subscription' === $data_type_id ) {
								$message .= ' For example, the order or subscription order could have been deleted. This issue may occur when a subscription order initially fails due to payment issues, but later succeeds. In such cases, the subscription order with the failed payment is deleted, but its workflow remains intact.';
							}
							Logger::info( 'queued-event', $message );
						}
						$uncompressed_data_layer[ $data_type_id ] = $decompressed_data;
					}
				}
			}

			$this->uncompressed_data_layer = new Data_Layer( $uncompressed_data_layer );
		}

		return $this->uncompressed_data_layer;
	}


	/**
	 * Fetches the data layer from queue meta, but does not decompress
	 * Uses the the supplied_data_items field on the workflows trigger
	 *
	 * @return array|false
	 */
	public function get_compressed_data_layer() {
		$workflow = $this->get_workflow();
		if ( ! $workflow ) {
			return false; // workflow must be set
		}

		if ( ! $this->exists ) {
			return false; // queue must be saved
		}

		$trigger = $workflow->get_trigger();
		if ( ! $trigger ) {
			return false; // need a trigger
		}

		$data_layer = [];

		$supplied_items = $trigger->get_supplied_data_items();

		foreach ( $supplied_items as $data_type_id ) {
			$data_item_value = $this->get_compressed_data_item( $data_type_id, $supplied_items );

			if ( $data_item_value !== false ) {
				$data_layer[ $data_type_id ] = $data_item_value;
			}
		}

		return $data_layer;
	}


	/**
	 * @param string $data_type_id        Data type object ID
	 * @param array  $supplied_data_items An array of supplied data items
	 *
	 * @return string|false
	 */
	private function get_compressed_data_item( $data_type_id, $supplied_data_items ) {
		if ( DataTypes::is_non_stored_data_type( $data_type_id ) ) {
			return false; // storage not required
		}

		$storage_key = Queue_Manager::get_data_layer_storage_key( $data_type_id );

		if ( ! $storage_key ) {
			return false;
		}

		return Clean::string( $this->get_meta( $storage_key ) );
	}


	/**
	 * Returns the workflow without a data layer
	 *
	 * @return Workflow|false
	 */
	public function get_workflow() {
		return Factory::get( $this->get_workflow_id() );
	}


	/**
	 * @return bool
	 */
	public function run() {
		if ( ! $this->exists ) {
			return false;
		}

		// Preserve queued events while the non-production environment lock is active,
		// so they are not failed or deleted and can run once workflows are re-enabled.
		if ( AdminNotices\NonProductionEnvironment::is_environment_locked() ) {
			return false;
		}

		// mark as failed and then delete if complete, so fatal error will not cause it to run repeatedly
		$this->mark_as_failed( self::F_FATAL_ERROR );
		$this->save();
		$success = false;

		$workflow = $this->get_workflow();
		$workflow->setup( $this->get_data_layer() );

		$failure = $this->do_failure_check( $workflow );

		if ( $failure ) {
			// queued event failed
			$this->mark_as_failed( $failure );
		} else {
			$success = true;

			// passed fail check so validate workflow and then delete
			if ( $this->validate_workflow( $workflow ) ) {
				$workflow->run();
			} else {
				// Order is no longer valid for this workflow so add a log before deleting the queued entry
				$workflow->create_run_log();

				$log = $workflow->get_current_log();

				$log->add_note( __( 'Queued Workflow: Run failed as requirements are no longer met for this workflow.', 'automatewoo' ) );
				$log->set_has_errors( true );
				$log->save();
			}

			$this->delete();
		}

		// important to always clean up
		$workflow->cleanup();
		return $success;
	}


	/**
	 * Returns false if no failure occurred
	 *
	 * @param Workflow $workflow
	 * @return bool|int
	 */
	public function do_failure_check( $workflow ) {
		if ( ! $workflow || ! $workflow->is_active() ) {
			return self::F_WORKFLOW_INACTIVE;
		}

		if ( $this->get_data_layer()->is_missing_data() ) {
			return self::F_MISSING_DATA;
		}

		return false;
	}


	/**
	 * Validate the workflow before running it from the queue.
	 * This validation is different from the initial trigger validation.
	 *
	 * @param Workflow $workflow The Workflow to validate
	 *
	 * @return bool
	 */
	public function validate_workflow( $workflow ) {
		$trigger = $workflow->get_trigger();
		if ( ! $trigger ) {
			return false;
		}

		if ( ! $trigger->validate_before_queued_event( $workflow ) ) {
			return false;
		}

		if ( $workflow->should_validate_rules_before_queued_run() && ! $workflow->validate_rules() ) {
			return false;
		}

		return true;
	}

	/**
	 * Clear cached workflow data stored as a transient `current_queue_count/workflow=WORKFLOW_ID`
	 *
	 * @return void
	 */
	public function clear_cached_data() {
		// Clear cached dashboard counts.
		Cache::flush_group( 'dashboard' );

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

		Cache::delete_transient( 'current_queue_count/workflow=' . $this->get_workflow_id() );
	}

	/**
	 * Save the current queued event
	 *
	 * @return void
	 */
	public function save() {
		if ( ! $this->exists ) {
			$this->set_date_created( new DateTime() );
		}

		parent::save();
	}

	/**
	 * @param int $code The error code to set for the current queued event
	 */
	public function mark_as_failed( $code ) {
		$this->set_failed();
		$this->set_failure_code( $code );
		$this->save();
	}

	/**
	 * Get failure message from failure code
	 *
	 * @return string
	 */
	public function get_failure_message() {
		return Queue_Manager::get_failure_message( $this->get_failure_code() );
	}


	/**
	 * Just for unit tests
	 */
	public function clear_in_memory_data_layer() {
		$this->uncompressed_data_layer = null;
	}
}
