<?php

/**
 * Bwfan Traversal Controller
 *
 * @since 1.0.0
 */
#[AllowDynamicProperties]
class BWFAN_Traversal_Controller {
	public $current_node_id = 0;

	/** For processing of conditional controller */
	public $automation_id = 0;
	public $contact_id = 0;
	public $trail_id = 0;

	/** Conditional traversal Result */
	public $conditional_result_node_id = null;

	/** Split test traversal result  */
	public $split_result_node_id = null;

	protected $steps = array();
	protected $links = array();

	/** Logging */
	protected $step_log = null;

	public $automation_contact_ins = null;

	/**
	 * Set steps in the traverser
	 *
	 * @param array $db_steps
	 *
	 * @return void
	 */
	public function set_steps( $db_steps ) {
		if ( ! is_array( $db_steps ) || empty( $db_steps ) ) {
			return;
		}

		foreach ( $db_steps as $step ) {
			$this->steps[ $step['id'] ] = $step;
		}
	}

	/**
	 * Set links in the traverser
	 *
	 * @param array $db_links
	 *
	 * @return void
	 */
	public function set_links( $db_links ) {
		if ( ! is_array( $db_links ) || empty( $db_links ) ) {
			return;
		}

		foreach ( $db_links as $link ) {
			if ( empty( $link ) || ! isset( $link['source'] ) || ! isset( $link['target'] ) ) {
				continue;
			}
			$this->links[ $link['source'] ] = $link['target'];
		}
	}

	/**
	 * Get current step id
	 *
	 * @return false|mixed
	 */
	public function get_current_step_id() {
		if ( empty( $this->steps ) || empty( $this->current_node_id ) || ! isset( $this->steps[ $this->current_node_id ] ) || ! isset( $this->steps[ $this->current_node_id ]['stepId'] ) ) {
			return false;
		}

		return $this->steps[ $this->current_node_id ]['stepId'];
	}

	/**
	 * Check if the current step is an end step
	 *
	 * @return bool
	 */
	public function is_end() {
		return $this->current_node_id === 'end';
	}

	/**
	 * @param array $step_ids Goal step IDs
	 * @param $goal_ins BWFAN_Goal_Controller
	 * @param array $automation_contact
	 *
	 * @return array|false
	 */
	public function try_traverse_to_goal( $step_ids, $goal_ins, $automation_contact ) {
		if ( empty( $this->current_node_id ) || empty( $step_ids ) ) {
			return false;
		}

		$goal_ins->goal_steps = $this->get_step_nodes( $step_ids );

		$run              = 0;
		$goal_found_nodes = [];
		while ( $run < count( $this->steps ) ) {
			$run ++;
			$current_step = $this->steps[ $this->current_node_id ];
			$this->log( 'current step', 'goal-check' );
			$this->log( [ 'id' => $current_step['id'], 'type' => $current_step['type'] ], 'goal-check' );

			switch ( $current_step['type'] ) {
				case 'action':
				case 'wait':
					$this->traverse_to_next_step();
					$this->log( 'traversed to next action/ wait step', 'goal-check' );
					break;
				case 'conditional':
					$this->process_goal_conditional( $goal_ins, $automation_contact );
					$this->traverse_to_next_step();
					$this->log( 'traversed to next conditional step', 'goal-check' );
					break;
				case 'jump':
					$this->process_jump( $goal_ins, $automation_contact );
					/** No need to traverse to next step as traversing already from process_jump function */
					$this->log( 'traversed to next jump step', 'goal-check' );
					break;
				case 'end':
					$this->log( 'end step', 'goal-check' );
					$run = count( $this->steps );
					break;
				case 'benchmark':
					if ( in_array( $current_step['id'], $goal_ins->goal_steps ) ) {
						$goal_found_nodes[] = $current_step['id'];
						$this->log( 'goal step found during traverse. goal step id: ' . $current_step['id'], 'goal-check' );
					}
					$this->traverse_to_next_step();
					$this->log( 'traversed to next step from goal step', 'goal-check' );
					break;
				case 'split':
					if ( in_array( $current_step['id'], $goal_ins->goal_steps ) ) {
						$goal_found_nodes[] = $current_step['id'];
						$this->log( 'split step found during traverse. split step id: ' . $current_step['id'], 'goal-check' );
					}
					$this->process_split_path( $automation_contact );
					$this->traverse_to_next_step();
					/** No need to traverse to next step as traversing already from process_jump function */
					$this->log( 'traversed to next split step', 'goal-check' );
					break;
				default:
					break;
			}
		}

		$goal_found_nodes = array_unique( $goal_found_nodes );
		sort( $goal_found_nodes );

		return $goal_found_nodes;
	}

	/**
	 * Return current step data by step id
	 *
	 * @param int $step_id
	 *
	 * @return false|mixed
	 */
	public function get_step_by_step_id( $step_id ) {
		if ( empty( $step_id ) ) {
			return false;
		}

		foreach ( $this->steps as $step ) {

			/** If step id is empty or is a split path then skip */
			if ( empty( $step['stepId'] ) || false !== strpos( (string) $step['stepId'], '-path-' ) ) {
				continue;
			}
			if ( ( absint( $step['stepId'] ) === absint( $step_id ) ) ) {
				return $step;
			}
		}

		return false;
	}

	/**
	 * Get node ids from step ids
	 *
	 * @param $step_ids
	 *
	 * @return array|false
	 */
	public function get_step_nodes( $step_ids ) {
		if ( empty( $step_ids ) ) {
			return false;
		}
		$nodes = [];
		foreach ( $this->steps as $step ) {
			if ( 'benchmark' !== $step['type'] ) {
				continue;
			}
			if ( isset( $step['stepId'] ) && in_array( $step['stepId'], $step_ids ) ) {
				$nodes[ $step['stepId'] ] = $step['id'];
			}
		}

		return empty( $nodes ) ? false : $nodes;

	}

	/**
	 * Traverse to next step node
	 *
	 * @return bool
	 */
	public function traverse_to_next_step() {
		$current_step = $this->get_current_step();
		$this->log( $this->trail_id . ' - current step id: ' . $current_step['stepId'] . ' and node id: ' . $current_step['id'] . ' and type: ' . $current_step['type'] );

		if ( empty( $current_step ) || empty( $this->links ) ) {
			return false;
		}

		if ( 'conditional' === $current_step['type'] ) {
			/** Traverse conditional step */
			$next_step = $this->traverse_conditional_step();
			if ( empty( $next_step ) ) {
				return false;
			}

			$this->current_node_id = $next_step;
			$this->log( $this->trail_id . ' - traverse to next step from conditional step. New node id: ' . $this->current_node_id );

			return true;
		}

		/** If current step is split */
		if ( 'split' === $current_step['type'] ) {
			/** Traverse split step */
			$next_step = $this->traverse_split_step();
			if ( empty( $next_step ) ) {
				return false;
			}

			$this->current_node_id = $next_step;
			$this->log( $this->trail_id . ' - traverse to next path from split step. New node id: ' . $this->current_node_id );

			return true;
		}

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

		$this->current_node_id = $this->links[ $this->current_node_id ];
		$this->log( $this->trail_id . ' - traverse to next step. New node id: ' . $this->current_node_id );

		return true;
	}

	/**
	 * Get current step data
	 *
	 * @return false|mixed
	 */
	public function get_current_step() {
		return ! empty( $this->steps ) && ! empty( $this->current_node_id ) && isset( $this->steps[ $this->current_node_id ] ) ? $this->steps[ $this->current_node_id ] : false;
	}

	/**
	 * Return step id after processing conditional step result
	 *
	 * @return false|mixed
	 */
	public function traverse_conditional_step() {
		$conditional_node_id = $this->conditional_result_node_id;
		if ( isset( $this->links[ $conditional_node_id ] ) ) {
			return $this->links[ $conditional_node_id ];
		}

		/** Maybe check if set in the database */
		if ( $this->automation_contact_ins instanceof BWFAN_Automation_Controller ) {
			$data = $this->automation_contact_ins->automation_contact_data;
			if ( isset( $data['node_id'] ) && isset( $this->links[ $data['node_id'] ] ) ) {
				/** Removing saved node id from the DB */
				$this->automation_contact_ins->update_conditional_step_result();

				return $this->links[ $data['node_id'] ];
			}
		}

		/** If somehow conditional step is deleted from table but its step node id exists in links then return target step of NO node */
		if ( isset( $this->links[ $this->links[ $this->current_node_id ] ] ) ) {
			return $this->links[ $this->links[ $this->current_node_id ] ];
		}

		BWFAN_Common::log_test_data( 'not able to traverse to condition next step ' . $this->current_node_id, 'traverse_conditional_failed' );
		BWFAN_Common::log_test_data( 'conditional_node_id: ' . $conditional_node_id, 'traverse_conditional_failed' );
		BWFAN_Common::log_test_data( $this->links, 'traverse_conditional_failed' );

		return false;
	}

	/**
	 * Return step id after processing split next path result
	 *
	 * @return false|mixed
	 */
	public function traverse_split_step() {
		$path_node_id = $this->split_result_node_id;
		if ( isset( $this->links[ $path_node_id ] ) ) {
			return $this->links[ $path_node_id ];
		}

		BWFAN_Common::log_test_data( 'not able to traverse to split next path ' . $this->current_node_id, 'traverse_split_failed' );
		BWFAN_Common::log_test_data( 'split_path_node_id: ' . $path_node_id, 'traverse_split_failed' );
		BWFAN_Common::log_test_data( $this->links, 'traverse_split_failed' );

		return false;
	}

	/**
	 * Process conditional step data for goal only
	 *
	 * @param $goal_ins BWFAN_Goal_Controller
	 * @param array $automation_contact automation contact row
	 *
	 * @return void
	 */
	public function process_goal_conditional( $goal_ins, $automation_contact ) {
		$current_step = $this->get_current_step();

		$ins                = new BWFAN_Conditional_Controller();
		$ins->contact_id    = $goal_ins->contact_id;
		$ins->automation_id = $goal_ins->automation_id;
		$ins->step_id       = absint( $current_step['stepId'] );
		$ins->populate_step_data( $current_step );
		$ins->populate_automation_contact_data( $automation_contact );

		$result = $ins->is_match();

		$this->conditional_result_node_id = $current_step['id'] . ( ( true === $result ) ? 'yes' : 'no' );
	}

	/**
	 * Process jump step
	 *
	 * @param $goal_ins
	 * @param $automation_contact
	 *
	 * @return void
	 */
	public function process_jump( $goal_ins, $automation_contact ) {
		$current_step = $this->get_current_step();

		$ins          = new BWFAN_Jump_Controller();
		$ins->step_id = absint( $current_step['stepId'] );
		$ins->populate_step_data( $current_step );

		$jump_step_id = $ins->get_jump_step_id();

		if ( empty( $jump_step_id ) ) {
			return;
		}

		/** Set current node to jump step */
		$this->set_node_id_from_step_id( $jump_step_id );
	}

	/**
	 * Maybe traverse back to valid step
	 *
	 * @return void
	 */
	public function maybe_traverse_back() {
		global $wpdb;

		/** @var BWFAN_Automation_Controller $ins */
		$ins = $this->automation_contact_ins;

		$step_id = $ins->step_id;
		$query   = $wpdb->prepare( "SELECT `ID` FROM `{$wpdb->prefix}bwfan_automation_contact_trail` WHERE `tid` = %s AND `sid` = %d LIMIT 0,1;", $ins->automation_contact['trail'], $step_id );
		$result  = $wpdb->get_var( $query ); //phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching

		$valid_step_id = 0;
		if ( ! empty( $result ) ) {
			$query  = $wpdb->prepare( "SELECT t.`sid` FROM `{$wpdb->prefix}bwfan_automation_contact_trail` as t INNER JOIN `{$wpdb->prefix}bwfan_automation_step` as s ON s.`ID` = t.`sid` WHERE t.`tid` = %s AND t.`ID` < %d AND s.`status` IN (%d, %d) ORDER BY t.ID DESC LIMIT 0,1;", $ins->automation_contact['trail'], $result, 0, 1 );
			$result = $wpdb->get_var( $query ); //phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
			if ( ! empty( $result ) ) {
				$valid_step_id = strval( $result );
			}
		}
		if ( empty( $valid_step_id ) ) {
			/** Step not found, start from the first node */
			$ins->step_id = $ins->automation['start'];
			$this->set_node_id_from_step_id( $ins->step_id );

			$ins->automation_contact['last'] = 0;

			return;
		}

		$this->set_node_id_from_step_id( $valid_step_id );
	}

	/**
	 * Set step node ID from the step ID
	 *
	 * @param int $step_id
	 *
	 * @return bool
	 */
	public function set_node_id_from_step_id( $step_id ) {
		$step = $this->get_step_by_step_id( $step_id );
		if ( empty( $step ) || ! isset( $step['id'] ) ) {
			return false;
		}

		$this->current_node_id = $step['id'];

		return true;
	}

	/**
	 * Process split step for goal only
	 *
	 * @param $automation_contact
	 *
	 * @return void
	 */
	public function process_split_path( $automation_contact ) {
		$current_step         = $this->get_current_step();
		$ins                  = new BWFAN_Split_Test_Controller();
		$ins->current_node_id = $this->current_node_id;
		$ins->automation_id   = $this->automation_id;
		$ins->step_id         = absint( $current_step['stepId'] );

		$ins->populate_step_data( $current_step );
		$ins->populate_automation_contact_data( $automation_contact );
		$path = $ins->get_next_path();

		$this->split_result_node_id = $current_step['id'] . '-path-' . $path;
	}

	/**
	 * Special log function for step execution
	 *
	 * @param $log
	 * @param $name
	 *
	 * @return void
	 */
	public function log( $log, $name = 'fka-automation-step-id' ) {
		if ( empty( $log ) ) {
			return;
		}
		if ( false === $this->step_log ) {
			return;
		}

		if ( is_null( $this->step_log ) && false === apply_filters( 'bwfan_allow_automation_step_logging', BWFAN_Common::is_log_enabled( 'bwfan_step_logging' ) ) ) {
			$this->step_log = false;

			return;
		}
		$this->step_log = true;

		BWFAN_Common::log_test_data( $log, $name, true );
	}
}
