<?php

namespace AutomateWoo;

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

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

/**
 * @class Log
 */
class Log extends Abstract_Model_With_Meta_Table {

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

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

	/** @var Data_Layer */
	private $data_layer;

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

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


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


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


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


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


	/**
	 * @param bool $has_errors
	 */
	public function set_has_errors( $has_errors ) {
		$this->set_prop( 'has_errors', aw_bool_int( $has_errors ) );
	}


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


	/**
	 * @param bool $has_blocked_emails
	 */
	public function set_has_blocked_emails( $has_blocked_emails ) {
		$this->set_prop( 'has_blocked_emails', absint( (bool) $has_blocked_emails ) );
	}


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


	/**
	 * @param bool $enabled
	 */
	public function set_tracking_enabled( $enabled ) {
		$this->set_prop( 'tracking_enabled', aw_bool_int( $enabled ) );
	}


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


	/**
	 * @param bool $enabled
	 */
	public function set_conversion_tracking_enabled( $enabled ) {
		$this->set_prop( 'conversion_tracking_enabled', aw_bool_int( $enabled ) );
	}


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


	/**
	 * @return bool
	 */
	public function is_anonymized() {
		return (bool) $this->get_meta( 'is_anonymized' );
	}


	/**
	 * Records that a trackable message from a workflow was clicked.
	 *
	 * @param string $url
	 */
	public function record_click( $url ) {

		$tracking = $this->get_meta( 'tracking_data' );
		if ( ! $tracking ) {
			$tracking = [];
		}

		$tracking[] = [
			'type' => 'click',
			'url'  => $url,
			'date' => current_time( 'mysql', true ),
		];

		// clicking requires an open so record one in case images were blocked
		if ( ! $this->has_open_recorded() ) {
			$tracking[] = [
				'type' => 'open',
				'date' => current_time( 'mysql', true ),
			];
		}

		$this->update_meta( 'tracking_data', $tracking );

		/**
		 * Fires when a link in a trackable workflow message is clicked.
		 *
		 * @param Workflow $workflow The workflow that sent the message.
		 * @param Log      $log      The record of the workflow running.
		 * @param string   $url      The URL that was clicked
		 */
		do_action( 'automatewoo/workflow/record_click', $this->get_workflow(), $this, $url );
	}


	/**
	 * Records that a trackable message from a workflow was opened.
	 *
	 * Only records an open once i.e. unique opens.
	 */
	public function record_open() {

		if ( $this->has_open_recorded() ) {
			return; // already opened
		}

		$tracking = $this->get_meta( 'tracking_data' );
		if ( ! $tracking ) {
			$tracking = [];
		}

		$tracking[] = [
			'type' => 'open',
			'date' => current_time( 'mysql', true ),
		];

		$this->update_meta( 'tracking_data', $tracking );

		/**
		 * Fires for unique opens of a trackable workflow message.
		 *
		 * @param Workflow $workflow The workflow that sent the message.
		 * @param Log      $log      The record of the workflow running.
		 */
		do_action( 'automatewoo/workflow/record_open', $this->get_workflow(), $this );
	}


	/**
	 * @return bool
	 */
	public function has_open_recorded() {

		$tracking = $this->get_meta( 'tracking_data' );

		if ( is_array( $tracking ) ) {
			foreach ( $tracking as $item ) {
				if ( $item['type'] === 'open' ) {
					return true;
				}
			}
		}

		return false;
	}


	/**
	 * @return bool
	 */
	public function has_click_recorded() {

		$tracking = $this->get_meta( 'tracking_data' );

		if ( is_array( $tracking ) ) {
			foreach ( $tracking as $item ) {
				if ( $item['type'] === 'click' ) {
					return true;
				}
			}
		}

		return false;
	}


	/**
	 * @return DateTime|false
	 */
	public function get_date_opened() {
		$tracking = $this->get_meta( 'tracking_data' );

		if ( is_array( $tracking ) ) {
			foreach ( $tracking as $item ) {
				if ( $item['type'] === 'open' ) {
					return new DateTime( $item['date'] );
				}
			}
		}

		return false;
	}


	/**
	 * @return DateTime|false
	 */
	public function get_date_clicked() {
		$tracking = $this->get_meta( 'tracking_data' );

		if ( is_array( $tracking ) ) {
			foreach ( $tracking as $item ) {
				if ( $item['type'] === 'click' ) {
					return new DateTime( $item['date'] );
				}
			}
		}

		return false;
	}

	/**
	 * Add a note.
	 *
	 * @param string $note
	 */
	public function add_note( $note ) {
		$notes   = $this->get_notes();
		$notes[] = $note;
		$this->update_notes( $notes );
	}

	/**
	 * Get the log notes.
	 *
	 * @since 4.4.0
	 *
	 * @return array
	 */
	public function get_notes() {
		$notes = $this->get_meta( 'notes' );
		if ( ! is_array( $notes ) ) {
			return [];
		} else {
			return Clean::recursive( $notes );
		}
	}

	/**
	 * Update the log notes.
	 *
	 * @since 4.4.0
	 *
	 * @param array $notes
	 */
	public function update_notes( $notes ) {
		$notes = Clean::recursive( $notes );
		$this->update_meta( 'notes', $notes );
	}


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


	/**
	 * @param string $output - array|object this for backwards compatibility
	 * @return Data_Layer|array
	 */
	public function get_data_layer( $output = 'array' ) {

		if ( ! isset( $this->data_layer ) ) {
			$compressed = $this->get_compressed_data_layer();
			if ( $compressed ) {
				$this->data_layer = $this->decompress_data_layer( $compressed );
			} else {
				$this->data_layer = new Data_Layer();
			}
		}

		if ( $output === 'array' ) {
			return $this->data_layer->get_raw_data();
		}

		return $this->data_layer;
	}


	/**
	 * Fetches the data layer from log meta, but does not decompress
	 * Uses the the supplied_data_items field on the workflows trigger
	 *
	 * @return array|false
	 */
	private function get_compressed_data_layer() {

		$workflow = $this->get_workflow();
		if ( ! $workflow ) {
			return false; // workflow must be set
		}

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

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

		$data_layer     = [];
		$supplied_items = $trigger->get_supplied_data_items();

		// when anonymized log is converted to guest
		if ( $this->is_anonymized() ) {
			$supplied_items[] = 'guest';
		}

		foreach ( $supplied_items as $data_type_id ) {

			$data_item_value = $this->get_compressed_data_item( $data_type_id, $trigger->get_supplied_data_items() );

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

		return $data_layer;
	}


	/**
	 * @param string $data_type_id
	 * @param array  $supplied_data_items
	 * @return string|int|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
		}

		// user requires special logic when related to an order
		if ( $data_type_id === 'user' && in_array( 'order', $supplied_data_items, true ) ) {
			return 0; // get user data from the order when decompressing
		}

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

		if ( ! $storage_key ) {
			return false;
		}

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


	/**
	 * @param array $compressed_data_layer
	 * @return Data_Layer
	 */
	private function decompress_data_layer( $compressed_data_layer ) {

		$data = [];

		if ( is_array( $compressed_data_layer ) ) {
			foreach ( $compressed_data_layer as $data_type_id => $compressed_item ) {
				$data_type = DataTypes::get( $data_type_id );
				if ( $data_type ) {
					$data[ $data_type_id ] = $data_type->decompress( $compressed_item, $compressed_data_layer );
				}
			}
		}

		return new Data_Layer( $data );
	}


	/**
	 * Stores a data layer in log meta
	 *
	 * @param Data_Layer $data_layer
	 */
	public function store_data_layer( $data_layer ) {

		if ( ! $this->exists ) {
			return; // log must be saved before meta can be added
		}

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


	/**
	 * @param string $data_type_id
	 * @param mixed  $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;
		}

		// special logic for users who are actually guests
		if ( $data_type_id === 'user' && $data_item->ID === 0 ) {
			$storage_key   = 'guest_email';
			$storage_value = $data_item->user_email;
		} else {
			$storage_key   = Logs::get_data_layer_storage_key( $data_type_id );
			$storage_value = Logs::get_data_layer_storage_value( $data_type_id, $data_item );
		}

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


	/**
	 * Delete the log and clear related conversion order meta
	 */
	public function delete() {

		// Get orders with conversion data.
		$converted_orders = wc_get_orders(
			[
				'type'       => 'shop_order',
				'status'     => 'any',
				'limit'      => -1,
				'meta_key'   => '_aw_conversion_log',
				'meta_value' => $this->get_id(),
			]
		);

		if ( $converted_orders ) {
			foreach ( $converted_orders as $order ) {
				$order->delete_meta_data( '_aw_conversion' );
				$order->delete_meta_data( '_aw_conversion_log' );
				$order->save();
			}
		}

		parent::delete();
	}

	/**
	 * Clear cached data related to this log.
	 */
	public function clear_cached_data() {

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

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


	/**
	 * Reruns the workflow skipping validation
	 *
	 * @return Log|bool - the newly created log
	 */
	public function rerun() {
		$workflow = $this->get_workflow();
		$workflow->maybe_run( $this->get_data_layer( 'object' ), true, true );

		$log = $workflow->get_current_log();

		if ( $log ) {
			$log->add_note( __( 'This log was created from manual workflow re-run.', 'automatewoo' ) );
			return $log;
		}

		return false;
	}
}
