<?php
namespace Uncanny_Automator\Logger;

use Uncanny_Automator\Services\Resolver\Recipe_Actions_Resolver;
use Uncanny_Automator\Automator_Functions;
use Uncanny_Automator\Resolver\Conditions\Errors_Mapping;
use Uncanny_Automator\Resolver\Conditions\Errors_Registry;
use Uncanny_Automator\Resolver\Fields_Resolver;
use Uncanny_Automator\Rest\Endpoint\Log_Endpoint\Utils\Formatters_Utils;

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

/**
 * Logs trigger fields to the database.
 *
 * This function resolves trigger fields using the Fields_Resolver class and then logs them to the database
 * using the Trigger_Fields_Logger class.
 *
 * @param int[] $hook_args The hook arguments containing the trigger and log data.
 *
 * @return void
 */
function trigger_fields_logger( $hook_args = array() ) {

	// Default values.
	$hook_args = wp_parse_args(
		$hook_args,
		array(
			'trigger_id'     => 0,
			'recipe_id'      => 0,
			'trigger_log_id' => 0,
			'user_id'        => 0,
		)
	);

	// Initialize run number with value of 1. Run number can be null but it can't be 0.
	$run_number = 1;

	// Check if run number is set in hook arguments, and update the $run_number variable if it is.
	if ( isset( $hook_args['run_number'] ) ) {
		$run_number = absint( $hook_args['run_number'] );
	}

	/** @var int[] $args */
	$args = array(
		'trigger_id'     => absint( $hook_args['trigger_id'] ),
		'recipe_id'      => absint( $hook_args['recipe_id'] ),
		'trigger_log_id' => absint( $hook_args['trigger_log_id'] ),
		'run_number'     => $run_number, // $run_number has already been sanitized.
		'user_id'        => absint( $hook_args['user_id'] ),
	);

	// Resolve the fields with Fields_Resolver class.
	$field_resolver = new Fields_Resolver();

	// Set the recipe ID, object type, and object ID for the field resolver.
	$field_resolver->set_recipe_id( $args['recipe_id'] )
			->set_object_type( 'trigger' )
			->set_object_id( $args['trigger_id'] );

	// Resolve the fields for the trigger.
	$fields = $field_resolver->resolve_object_fields();

	// Log the fields using the Trigger_Fields_Logger.
	$trigger_fields_logger = new Trigger_Fields_Logger();
	$trigger_fields_logger->log( $args, $fields );
}

/**
 * Logs fields related to a specific action
 *
 * This function requires fields-resolver.php and action-fields-logger.php files.
 *
 * @param int[] $hook_args Arguments passed to the hook that triggers the function
 *
 * Required keys:
 * - action_log_id (int): ID of the action log
 * - action_id (int): ID of the action
 * - user_id (int): ID of the user
 *
 * @return void
 */
function action_fields_logger( $hook_args = array() ) {

	$hook_args = wp_parse_args(
		$hook_args,
		array(
			'action_log_id' => 0,
			'action_id'     => 0,
			'recipe_id'     => 0,
			'user_id'       => 0,
		)
	);

	/**
	 * This method is attached into two action hooks: "automator_action_created", and "automator_pro_async_action_execution_after_invoked"
	 *
	 * Action hook "automator_action_created" runs regardless whether the action is scheduled or not.
	 *
	 * The problem is that when the admin edits the action while the Action is delayed, it already saves the field values.
	 *
	 * Therefore it was hook into "automator_pro_async_action_execution_after_invoked" so that it will be log again when the action is run from delay.
	 *
	 * The hooks have different arguments therefore we need to resolve it.
	 */
	if ( 'automator_pro_async_action_execution_after_invoked' === current_action() ) {
		$hook_args['user_id']   = $hook_args['args']['user_id'];
		$hook_args['action_id'] = $hook_args['ID'];
		$hook_args['recipe_id'] = $hook_args['args']['recipe_id'];
	}

	// Extract required arguments.
	$args = array(
		'user_id'       => absint( $hook_args['user_id'] ),
		'action_id'     => absint( $hook_args['action_id'] ),
		'action_log_id' => absint( $hook_args['action_log_id'] ),
	);

	// Create an instance of Fields_Resolver class
	$fields_resolver = new Fields_Resolver();

	// Set the required properties of Fields_Resolver instance
	$fields_resolver->set_recipe_id( absint( $hook_args['recipe_id'] ) );
	$fields_resolver->set_object_id( absint( $hook_args['action_id'] ) );
	$fields_resolver->set_object_type( 'action' );

	// Create an instance of Action_Fields_Logger class
	$action_fields_logger = new Action_Fields_Logger();

	// Call the log method of Action_Fields_Logger instance
	// with extracted arguments and resolved fields
	$action_fields_logger->log( $args, $fields_resolver->resolve_object_fields() );
}

/**
 * Log the triggers of a recipe.
 *
 * @param mixed[] $hook_args The arguments passed to the hook.
 *
 * - recipe_id (int) (Required): The ID of the recipe being triggered.
 * - trigger_id (int) (Required): The ID of the trigger being logged.
 * - trigger_log_id (int) (Required): The ID of the trigger log.
 * - run_number (int) (Optional): The number of times the recipe has been triggered. Default is 1.
 * - user_id (int) (Required): The ID of the user triggering the recipe.
 *
 * @return void
 */
function recipe_triggers_logger( $hook_args = array() ) {

	// Get the data for the recipe being triggered.
	$recipe_data = Automator()->get_recipes_data( false, absint( $hook_args['recipe_id'] ) );

	$current_triggers = array();

	// Retrieve the triggers for the recipe.
	foreach ( $recipe_data as $recipe_id => $recipe ) {
		if ( is_array( $recipe['triggers'] ) && ! empty( $recipe['triggers'] ) ) {
			$triggers_live    = array_filter(
				$recipe['triggers'],
				function ( $trigger_item ) {
					return 'publish' === $trigger_item['post_status'];
				}
			);
			$current_triggers = array_column( $triggers_live, 'ID' );
		}
	}

	// Initialize the run number with a default value of 1.
	$run_number = 1;

	// If the run number is specified, use that instead of the default.
	if ( isset( $hook_args['run_number'] ) ) {
		$run_number = $hook_args['run_number'];
	}

	/**
	 * @var int[] $args
	 */
	$args = array(
		'trigger_id'     => absint( $hook_args['trigger_id'] ),
		'recipe_id'      => absint( $hook_args['recipe_id'] ),
		'trigger_log_id' => absint( $hook_args['trigger_log_id'] ),
		'run_number'     => absint( $run_number ),
		'user_id'        => absint( $hook_args['user_id'] ),
	);

	// Instantiate the Recipe Objects Logger.
	$recipe_objects_logger = new Recipe_Objects_Logger();

	// Set the key for the logger.
	$recipe_objects_logger->set_key( 'recipe_current_triggers' );

	// Log the triggers.
	$trigger_ids = wp_json_encode( $current_triggers );
	if ( false === $trigger_ids ) {
		$trigger_ids = '';
	}
	$recipe_objects_logger->log_triggers( $args, $trigger_ids );
}

/**
 * Logs the actions flow for a recipe.
 *
 * @param mixed[] $hook_args The arguments passed to the hook.
 *
 * @return void
 */
function recipe_actions_flow_logger( $hook_args = array() ) {

	// Prepare arguments for logging
	$args = array(
		'user_id'       => absint( $hook_args['user_id'] ),
		'recipe_id'     => absint( $hook_args['recipe_id'] ),
		'recipe_log_id' => absint( $hook_args['recipe_log_id'] ),
	);

	// Create resolver and set recipe id
	$resolver = new Recipe_Actions_Resolver( Automator_Functions::get_instance() );
	$resolver->set_recipe_id( $args['recipe_id'] );

	// Create recipe objects logger
	$recipe_objects_logger = new Recipe_Objects_Logger();

	// Log actions flow
	$recorded = $recipe_objects_logger->get_meta( $args, 'actions_flow' );

	if ( empty( $recorded ) ) {
		$recipe_objects_logger->log_actions_flow( $args, $resolver->resolve_recipe_actions() );
	}
}

/**
 * @param mixed[] $hook_args
 *
 * @return void
 */
function closure_logger( $hook_args ) {

	// Prepare arguments for logging
	$args = array(
		'user_id'       => absint( $hook_args['user_id'] ),
		'recipe_id'     => absint( $hook_args['recipe_id'] ),
		'recipe_log_id' => absint( $hook_args['recipe_log_id'] ),
	);

	$logger = new Recipe_Objects_Logger();

	global $wpdb;

	$result = $wpdb->get_row(
		$wpdb->prepare(
			"SELECT post.ID, meta.meta_value FROM {$wpdb->posts} as post
			INNER JOIN {$wpdb->postmeta} as meta
			ON post.ID = meta.post_id
			WHERE meta.meta_key = 'REDIRECTURL'
			AND post.post_parent = %d
			AND post.post_type = %s
			",
			$args['recipe_id'],
			AUTOMATOR_POST_TYPE_CLOSURE
		),
		ARRAY_A
	);

	if ( ! empty( $result ) ) {
		$recorded = $logger->get_meta( $args, 'closures' );
		if ( empty( $recorded ) ) {
			$closure_meta = Formatters_Utils::flatten_post_meta( (array) get_post_meta( $result['ID'] ) );
			$logger->add_meta(
				$args,
				'closures',
				array(
					'log'  => $result,
					'meta' => $closure_meta,
				)
			);
		}
	}
}

/**
 * Logs the conditions for a recipe action that failed.
 *
 * @param mixed[] $action The action data.
 * @param string $code The error code for the failed condition.
 * @param string $message The error message for the failed condition.
 *
 * @return void
 */
function recipe_actions_conditions_logger( $action = array(), $code = '', $message = '' ) {

	$action = wp_parse_args(
		$action,
		array(
			'action_data' => array(
				'ID'                        => 0,
				'failed_actions_conditions' => array(),
				'actions_conditions_log'    => array(),
				'args'                      => array(
					'recipe_id' => null,
				),
			),
		)
	);

	if ( empty( $action['action_data']['failed_actions_conditions'] ) ) {
		return;
	}

	if ( empty( $action['action_data']['actions_conditions_log'] ) ) {
		return;
	}

	// Load necessary files
	if ( ! isset( $action['action_data']['args'] ) ) {
		return;
	}

	// Get recipe id
	$recipe_id = absint( $action['action_data']['args']['recipe_id'] );

	// Create errors registry and errors mapping
	$error_registry = Errors_Registry::get_instance();
	$errors_mapping = new Errors_Mapping();

	// Set errors mapping source and action id
	$actions_conditions = get_post_meta( $recipe_id, 'actions_conditions', true );
	if ( ! is_string( $actions_conditions ) ) {
		$actions_conditions = '';
	}

	$errors_mapping->set_source( $actions_conditions );
	$errors_mapping->set_action_id( $action['action_data']['ID'] );

	// Get condition ids from error code and log errors
	$condition_ids = $errors_mapping->condition_ids_from_code( $code );

	foreach ( $condition_ids as $condition_id ) {
		if ( ! $error_registry->has_error( $condition_id ) ) {
			$error_registry->add( $condition_id, $message );
			// Break from the loop so only the first condition that "does not" contain the message gets populated.
			// Otherwise, the second message that comes up will be ignored.
			break;
		}
	}
}

/**
 * Logs any errors that occurred during recipe conditions check.
 *
 * @param int   $recipe_id     ID of the recipe being checked.
 * @param int   $user_id       ID of the user the recipe belongs to.
 * @param int   $recipe_log_id ID of the recipe log that triggered the condition check.
 *
 * @return void
 */
function conditions_errors_logger( $recipe_id = 0, $user_id = 0, $recipe_log_id = 0 ) {

	// Bail if there are no errors.
	$error_registry = Errors_Registry::get_instance();

	// Initialize the recipe objects logger.
	$recipe_objects_logger = new Recipe_Objects_Logger();

	// Set up the arguments for logging.
	$args = array(
		'user_id'       => $user_id,
		'recipe_id'     => $recipe_id,
		'recipe_log_id' => $recipe_log_id,
	);

	// Add the errors to the recipe objects logger as meta data.
	$recipe_objects_logger->add_meta( $args, 'conditions_failed', $error_registry->get_errors() );
}

/**
 * Logs tokens for a given integration and code.
 *
 * @param mixed[] $hook_args Arguments passed to the hook.
 *
 * @return void
 */
function tokens_logger( $hook_args = array() ) {

	$hook_args = wp_parse_args(
		$hook_args,
		array(
			'args'          => array(
				'recipe_id'  => null,
				'run_number' => 1,
			),
			'recipe_log_id' => null,
		)
	);

	// Retrieve the tokens for the given integration and code.
	$parsed_token_records = Automator()->parsed_token_records();
	$tokens_record        = $parsed_token_records->get_tokens();

	// Get the recipe ID, run number, and recipe log ID from the hook arguments.
	$recipe_id     = $hook_args['args']['recipe_id'];
	$run_number    = $hook_args['args']['run_number'];
	$recipe_log_id = $hook_args['recipe_log_id'];

	// If tokens exist, log them using the Tokens_Logger class.
	if ( is_array( $tokens_record ) && ! empty( $tokens_record ) ) {

		// Create a new instance of Tokens_Logger and log the tokens.
		$logger = new Tokens_Logger();

		$logger->log(
			array(
				'tokens_record' => $tokens_record,
				'recipe_id'     => $recipe_id,
				'recipe_log_id' => $recipe_log_id,
				'run_number'    => $run_number,
			)
		);
	}
}

/**
 * Logs gathered async entries.
 *
 * @param int $recipe_id
 * @param int $user_id
 * @param int $recipe_log_id
 * @param mixed[] $args
 *
 * @return void
 */
function log_async_actions( $recipe_id = 0, $user_id = 0, $recipe_log_id = 0, $args = array() ) {

	$entries = Automator()->async_action_logger()->get_entries();

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

	$recipe_objects_logger = new Recipe_Objects_Logger();

	// Prepare the arguments for the logger.
	$args = array(
		'recipe_log_id' => absint( $recipe_log_id ),
		'recipe_id'     => absint( $recipe_id ),
		'user_id'       => absint( $user_id ),
	);

	foreach ( $entries as $entry ) {
		if ( ! is_string( $entry ) ) {
			continue;
		}
		$entry_arr = (array) json_decode( $entry, true );
		if ( empty( $entry_arr['type'] ) ) {
			// Bail if schedule type is null or empty.
			continue;
		}
	}

	// Add the evaluated conditions to the recipe objects logger as meta data.
	$recipe_objects_logger->add_meta( $args, 'action_delays', $entries );
}

/**
 * Collects async actions. This does not save data to the db.
 *
 * @param mixed[] $action
 *
 * @return void
 */
function collect_async_actions( $action = array() ) {

	$action = wp_parse_args(
		$action,
		array(
			'action_data' => array(
				'ID'    => 0,
				'async' => array(
					'mode'      => null,
					'timestamp' => null,
				),
			),
		)
	);

	$action_id = $action['action_data']['ID'];

	$type       = isset( $action['action_data']['async']['mode'] ) ? $action['action_data']['async']['mode'] : null;
	$time_stamp = isset( $action['action_data']['async']['timestamp'] ) ? $action['action_data']['async']['timestamp'] : null;

	$data = array(
		'type' => $type,
		'time' => $time_stamp,
	);

	if ( 'delay' === $type ) {
		$data['time_number'] = get_post_meta( $action_id, 'async_delay_number', true );
		$data['time_unit']   = get_post_meta( $action_id, 'async_delay_unit', true );
	}

	Automator()->async_action_logger()->add_entry( $action_id, wp_json_encode( $data ) );
}

/**
 * Logs triggers logic during recipe completed and num times insufficent.
 *
 * @param mixed[] $args
 *
 * @return void
 */
function recipe_triggers_logic_logger( $args ) {

	$recipe_objects_logger = new Recipe_Objects_Logger();

	$recipe_id = $args['recipe_id'];

	$logic = get_post_meta( $recipe_id, 'automator_trigger_logic', true );

	if ( empty( $logic ) ) {
		$logic = 'all';
	}

	$logger_args = array(
		'recipe_id'     => $recipe_id,
		'user_id'       => $args['user_id'],
		'recipe_log_id' => $args['recipe_log_id'],
	);

	$recipe_objects_logger->add_meta(
		$logger_args,
		'triggers_logic',
		array(
			'logic' => $logic,
		),
		true // <-- Upserts
	);
}

/**
 * Registers listeners to log various events related to recipe triggers and actions.
 *
 * @return void
 */
function fields_logger_register_listeners() {

	// Record field during insufficient num times with the same hooks and callbacks.
	add_action( 'automator_recipe_process_user_trigger_num_times_insufficient', '\Uncanny_Automator\Logger\trigger_fields_logger', 10, 1 );
	add_action( 'automator_recipe_process_user_trigger_num_times_insufficient', '\Uncanny_Automator\Logger\recipe_triggers_logger', 20, 1 );
	add_action( 'automator_recipe_process_user_trigger_num_times_insufficient', '\Uncanny_Automator\Logger\recipe_actions_flow_logger', 30, 1 );
	add_action( 'automator_recipe_process_user_trigger_num_times_insufficient', '\Uncanny_Automator\Logger\closure_logger', 40, 1 );
	add_action( 'automator_recipe_process_user_trigger_num_times_insufficient', '\Uncanny_Automator\Logger\recipe_triggers_logic_logger', 50, 1 );

	// Record trigger - recipe_triggers_logger().
	// Record field during trigger completed - trigger_fields_logger().
	// Record actions through trigger completed because there can be multiple triggers "in-progress" - recipe_actions_flow_logger().
	add_action( 'automator_trigger_completed', '\Uncanny_Automator\Logger\recipe_triggers_logger', 10, 1 );
	add_action( 'automator_trigger_completed', '\Uncanny_Automator\Logger\trigger_fields_logger', 20, 1 );
	add_action( 'automator_trigger_completed', '\Uncanny_Automator\Logger\recipe_actions_flow_logger', 30, 1 );
	add_action( 'automator_trigger_completed', '\Uncanny_Automator\Logger\closure_logger', 40, 1 );
	add_action( 'automator_trigger_completed', '\Uncanny_Automator\Logger\recipe_triggers_logic_logger', 50, 1 );

	// Log all conditions error after the actions has been completed. Just before the closures begin executing.
	// Log all gathered async actions.
	add_action( 'automator_recipe_process_complete_complete_actions_before_closures', '\Uncanny_Automator\Logger\conditions_errors_logger', 10, 3 );
	add_action( 'automator_recipe_process_complete_complete_actions_before_closures', '\Uncanny_Automator\Logger\log_async_actions', 20, 4 );

	// Record field at the start of the action process.
	// Log tokens.
	add_action( 'automator_action_created', '\Uncanny_Automator\Logger\action_fields_logger', 10, 1 );
	add_action( 'automator_action_created', '\Uncanny_Automator\Logger\tokens_logger', 10, 1 );

	// Collects the async actions.
	add_action( 'automator_action_been_process_further', '\Uncanny_Automator\Logger\collect_async_actions' );
};
