<?php
/**
 * Gravity Flow Step: Choice Routing
 *
 * Routes the workflow to different next steps based on the value of a
 * selected form field, using a single routing rules table (multiple choices
 * per row map to one next step) and the dynamic_choice_map setting type.
 *
 * @package     GravityFlow
 * @subpackage  Classes/Gravity_Flow_Step_Choice_Routing
 * @since       next
 */

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

if ( ! class_exists( 'GFForms' ) ) {
	return;
}

/**
 * Class Gravity_Flow_Step_Choice_Routing
 */
class Gravity_Flow_Step_Choice_Routing extends Gravity_Flow_Step {

	/**
	 * The step type.
	 *
	 * @since 3.1.0
	 *
	 * @var string
	 */
	public $_step_type = 'choice_routing';

	/**
	 * Cache for resolved step targets.
	 *
	 * @since 3.1.0
	 *
	 * @var array
	 */
	private $_resolved_targets = array();

	/**
	 * Returns the step label.
	 *
	 * @since 3.1.0
	 *
	 * @return string
	 */
	public function get_label() {
		return esc_html__( 'Choice Routing', 'gravityflow' );
	}

	/**
	 * Returns the HTML for the step icon.
	 *
	 * @since 3.1.0
	 *
	 * @return string
	 */
	public function get_icon_url() {
		return '<i class="fa fa-code-fork"></i>';
	}

	/**
	 * Returns an array of settings for this step type.
	 *
	 * @since 3.1.0
	 *
	 * @return array The step settings configuration.
	 */
	public function get_settings() {
		$form    = $this->get_form();
		$choices = array(
			array(
				'label' => esc_html__( 'Select a field', 'gravityflow' ),
				'value' => '',
			),
		);

		if ( ! empty( $form ) && is_array( $form ) && ! empty( $form['fields'] ) && is_array( $form['fields'] ) ) {
			foreach ( $form['fields'] as $field ) {
				if ( in_array( $field->type, array( 'select', 'radio' ), true ) && ! empty( $field->choices ) && is_array( $field->choices ) ) {
					$choices[] = array(
						'label' => GFCommon::get_label( $field ),
						'value' => $field->id,
					);
				}
			}
		}

		$option_choices = $this->get_routing_field_option_choices( $form );
		$target_choices = $this->get_step_target_choices();

		$fields = array(
			array(
				'name'                => 'routing_field',
				'label'               => esc_html__( 'Choice Field', 'gravityflow' ),
				'type'                => 'select',
				'tooltip'             => esc_html__( 'Select the dropdown or radio field whose choice value will determine the next step. Routing options will appear after you select a field.', 'gravityflow' ),
				'choices'             => $choices,
				'default_value'       => '',
				'required'            => true,
				'validation_callback' => array( 'Gravity_Flow_Step_Choice_Routing', 'validate_routing_field' ),
				'onchange'            => 'jQuery(this).parents("form").submit();',
			),
			array(
				'name'          => 'routing_rules',
				'label'         => esc_html__( 'Routes', 'gravityflow' ),
				'type'          => 'dynamic_choice_map',
				'tooltip'       => esc_html__( 'Add rows to map one or more field choice to a next step. Each choice may only appear in one routing row.', 'gravityflow' ),
				'options'       => $option_choices,
				'target'        => $target_choices,
				'options_label' => esc_html__( 'Choices', 'gravityflow' ),
				'target_label'  => esc_html__( 'Step', 'gravityflow' ),
				'dependency'    => array(
					'field'  => 'routing_field',
					'values' => array( '_notempty_' ),
				),
			),
		);

		return array(
			'title'  => esc_html__( 'Choice Routing', 'gravityflow' ),
			'fields' => $fields,
		);
	}

	/**
	 * Get option choices from the currently selected routing field.
	 *
	 * @since 3.1.0
	 *
	 * @param array $form The current form.
	 *
	 * @return array Array of { label, value } for dynamic_choice_map options column.
	 */
	protected function get_routing_field_option_choices( $form ) {
		$routing_field_id = $this->get_setting( 'routing_field' );
		if ( empty( $routing_field_id ) || empty( $form ) || ! is_array( $form ) ) {
			return array();
		}

		$field = GFFormsModel::get_field( $form, $routing_field_id );
		if ( ! $field || empty( $field->choices ) || ! is_array( $field->choices ) ) {
			return array();
		}
		$out = array();
		foreach ( $field->choices as $choice ) {
			$value = rgar( $choice, 'value' );
			$text  = rgar( $choice, 'text', $value );
			$out[] = array( 'label' => $text, 'value' => (string) $value );
		}
		return $out;
	}

	/**
	 * Get step choices for the target column (Complete, Next, then each step).
	 *
	 * @since 3.1.0
	 *
	 * @return array Array of { label, value } for dynamic_choice_map target column.
	 */
	protected function get_step_target_choices() {
		$form_id = $this->get_form_id();
		if ( empty( $form_id ) ) {
			return array();
		}

		$steps = gravity_flow()->get_steps( $form_id );

		$out = array();

		$current_step_id = gravity_flow()->get_current_feed_id();

		$out[] = array( 'label' => esc_html__( 'Next step in list', 'gravityflow' ), 'value' => 'next' );

		if ( ! empty( $steps ) && is_array( $steps ) ) {
			foreach ( $steps as $step ) {
				$step_id = $step->get_id();
				if ( (string) $step_id !== (string) $current_step_id ) {
					$out[] = array( 'label' => $step->get_name(), 'value' => (string) $step_id );
				}
			}
		}

		$out[] = array( 'label' => esc_html__( 'Workflow Complete', 'gravityflow' ), 'value' => 'complete' );

		return $out;
	}

	/**
	 * Validation callback: routing field is required for Choice Routing steps.
	 *
	 * @since 3.1.0
	 *
	 * @param array  $field  The field config.
	 * @param string $value  The submitted value.
	 *
	 * @return void
	 */
	public static function validate_routing_field( $field, $value ) {
		if ( $value === '' || $value === null ) {
			gravity_flow()->set_field_error( $field, esc_html__( 'Please select a routing field. The step cannot be saved without one.', 'gravityflow' ) );
		}
	}

	/**
	 * Returns the status configuration for the Choice Routing step.
	 * The destination setting serves as a fallback route when no routing rules match.
	 *
	 * @since 3.1.0
	 *
	 * @return array
	 */
	public function get_status_config() {
		return array(
			array(
				'status'                    => 'complete',
				'status_label'              => esc_html__( 'Complete', 'gravityflow' ),
				'destination_setting_label' => esc_html__( 'Default Step Route', 'gravityflow' ),
				'default_destination'       => 'next',
			),
		);
	}

	/**
	 * Resolves a step target to a valid active destination.
	 * Falls back to the configured fallback if the target step is inactive or not found.
	 * Results are cached to avoid repeat db calls.
	 *
	 * @since 3.1.0
	 *
	 * @param string $target   Valid step 'id', 'next', or 'complete'.
	 * @param string $fallback The fallback destination if the target is invalid. Defaults to 'next'.
	 *
	 * @return string Valid step 'id', 'next', or 'complete'.
	 */
	protected function resolve_target( $target, $fallback = 'next' ) {
		if ( ! is_numeric( $target ) ) {
			return $target;
		}
		if ( ! isset( $this->_resolved_targets[ $target ] ) ) {
			$step = gravity_flow()->get_step( $target );

			$this->_resolved_targets[ $target ] = ( $step && $step->is_active() ) ? $target : $fallback;
			if ( $this->_resolved_targets[ $target ] !== $target ) {
				$this->log_debug( __METHOD__ . '() - Target step ' . $target . ' is inactive or no longer exists, routing to configured default step route.' );
			}
		}
		return $this->_resolved_targets[ $target ];
	}

	/**
	 * Resolve next step id from routing_rules and entry value.
	 * Falls back to the configured default destination if no rule matches.
	 *
	 * @since 3.1.0
	 *
	 * @return string Next step 'id', 'next', or 'complete'.
	 */
	public function get_next_step_id() {
		if ( isset( $this->_next_step_id ) ) {
			return $this->_next_step_id;
		}

		$default_destination = $this->resolve_target( isset( $this->destination_complete ) ? $this->destination_complete : 'next' );

		$routing_field_id = isset( $this->routing_field ) ? $this->routing_field : '';
		if ( empty( $routing_field_id ) ) {
			$this->log_debug( __METHOD__ . '() - No routing field configured, routing to configured default step route.' );
			$this->set_next_step_id( $default_destination );
			return $default_destination;
		}

		$entry = $this->get_entry();
		if ( empty( $entry ) ) {
			$this->log_debug( __METHOD__ . '() - No entry found, routing to configured default step route.' );
			$this->set_next_step_id( $default_destination );
			return $default_destination;
		}

		$value = rgar( $entry, (string) $routing_field_id );
		$value = $value === '' || $value === null ? null : (string) $value;
		$this->log_debug( __METHOD__ . '() - Routing field value: ' . $value );

		$rules = isset( $this->routing_rules ) && is_array( $this->routing_rules ) ? $this->routing_rules : array();
		if ( is_string( $this->routing_rules ) ) {
			$decoded = json_decode( $this->routing_rules, true );
			$rules   = is_array( $decoded ) ? $decoded : array();
		}

		$next = $default_destination;
		if ( $value !== null ) {
			foreach ( $rules as $row ) {
				$option_values = isset( $row['option_values'] ) && is_array( $row['option_values'] )
					? $row['option_values']
					: array();
				if ( in_array( $value, $option_values, true ) ) {
					$next = $this->resolve_target( isset( $row['target'] ) ? $row['target'] : $default_destination, $default_destination );
					break;
				}
			}
		}

		$this->log_debug( __METHOD__ . '() - Resolved next step: ' . $next );
		$this->set_next_step_id( $next );
		return $next;
	}

	/**
	 * Evaluates and returns the step status.
	 *
	 * @since 3.1.0
	 *
	 * @return string Always returns 'complete' as this step processes instantly.
	 */
	public function status_evaluation() {
		return 'complete';
	}

	/**
	 * Step completes when not queued (e.g. when sent via admin "Send to step").
	 *
	 * @since 3.1.0
	 *
	 * @return bool True if the step is complete, false if queued.
	 */
	public function is_complete() {
		$status = $this->evaluate_status();
		if ( $status === 'queued' ) {
			return false;
		}
		return true;
	}

	/**
	 * Process the step: add timeline note and let base flow use get_next_step_id().
	 *
	 * @since 3.1.0
	 *
	 * @return bool True so the step completes and end() runs.
	 */
	public function process() {
		$this->log_debug( __METHOD__ . '() - Starting. Entry ID: ' . $this->get_entry_id() );

		$routing_field_id = isset( $this->routing_field ) ? $this->routing_field : '';
		$entry            = $this->get_entry();
		$form             = $this->get_form();

		$choice_label = esc_html__( 'Empty', 'gravityflow' );
		if ( $routing_field_id && ! empty( $form['fields'] ) ) {
			$value = rgar( $entry, (string) $routing_field_id );
			if ( $value !== '' && $value !== null ) {
				$choice_label = $value;
				$field        = GFFormsModel::get_field( $form, $routing_field_id );
				if ( $field && ! empty( $field->choices ) ) {
					foreach ( $field->choices as $choice ) {
						if ( (string) rgar( $choice, 'value' ) === (string) $value ) {
							$choice_label = rgar( $choice, 'text', $value );
							break;
						}
					}
				}
			}
		}

		$next_step_id = $this->get_next_step_id();
		if ( $next_step_id && $next_step_id !== 'next' && $next_step_id !== 'complete' && is_numeric( $next_step_id ) ) {
			$next_step      = gravity_flow()->get_step( $next_step_id, $entry );
			$next_step_name = $next_step ? $next_step->get_name() : $next_step_id;
		} elseif ( $next_step_id === 'complete' ) {
			$next_step_name = esc_html__( 'Workflow Complete', 'gravityflow' );
		} else {
			$next_step_name = esc_html__( 'Next step in list', 'gravityflow' );
		}

		$note = sprintf(
			/* translators: 1: step name, 2: choice label from entry, 3: next step name */
			esc_html__( '%1$s: Routed %2$s to %3$s', 'gravityflow' ),
			$this->get_name(),
			$choice_label,
			$next_step_name
		);
		$this->add_note( $note );

		$this->log_debug( __METHOD__ . '() - Complete. Routed to: ' . $next_step_name );

		return true;
	}
}

Gravity_Flow_Steps::register( new Gravity_Flow_Step_Choice_Routing() );
