<?php
/**
 * Activity Functions
 *
 * @since 3.4.0
 *
 * @package LearnDash\Activity
 */

use LearnDash\Core\Utilities\Cast;
use StellarWP\Learndash\StellarWP\DB\DB;

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

/**
 * Updates the user activity.
 *
 * @since 2.3.0
 *
 * @global wpdb $wpdb WordPress database abstraction object.
 *
 * @param array $args {
 *    An array of user activity arguments. Default empty array.
 *
 *    @type int    $activity_id        Optional. Activity ID. Default 0.
 *    @type int    $course_id          Optional. Course ID. Default 0.
 *    @type int    $post_id            Optional. Post ID. Default 0.
 *    @type int    $user_id            Optional. User ID. Default 0.
 *    @type string $activity_type      Optional. Type of the activity. Default empty.
 *    @type string $activity_status    Optional. The status of the activity. Default empty.
 *    @type string $activity_started   Optional. The timestamp of when the activity started. Default empty.
 *    @type string $activity_completed Optional. The timestamp of when the activity got completed. Default empty.
 *    @type string $activity_updated   Optional. The timestamp of when the activity was last updated. Default empty.
 *    @type string $activity_action    Optional. The action of the activity. Value can be 'update' or 'insert'. Default 'update'.
 *    @type string $activity_meta      Optional. The activity meta. Default empty.
 * }
 *
 * @return int The ID of the updated activity.
 */
function learndash_update_user_activity( $args = array() ) {

	global $wpdb;

	if ( is_object( $args ) ) {
		$args = json_decode( wp_json_encode( $args ), true );
	}

	$default_args = array(
		// Can be passed in if we are updating a specific existing activity row.
		'activity_id'        => 0,

		// Required. This is the ID of the Course. Unique key part 1/4.
		'course_id'          => 0,

		// Required. This is the ID of the Course, Lesson, Topic, Quiz item. Unique key part 2/4.
		'post_id'            => 0,

		// Optional. Will use get_current_user_id() if left 0. Unique key part 3/4.
		'user_id'            => 0,

		// Will be the token stats that described the status_times array (next argument) Can be most anything.
		// From 'course', 'lesson', 'topic', 'access', 'group_progress', 'exam', or 'expired'. Unique key part 4/4.
		'activity_type'      => '',

		// true if the lesson, topic, course, quiz is complete. False if not complete. null if not started.
		'activity_status'    => '',

		// Should be the timestamp when the 'status' started.
		'activity_started'   => '',

		// Should be the timestamp when the 'status' completed.
		'activity_completed' => '',

		// Should be the timestamp when the activity record was last updated. Used as a sort column for ProPanel and other queries.
		'activity_updated'   => '',

		// Flag to indicate what we are 'update', 'insert', 'delete'. The default action 'update' will cause this function
		// to check for an existing record to update (if found).
		'activity_action'    => 'update',
		'activity_meta'      => '',
	);

	$args = wp_parse_args( $args, $default_args );
	if ( empty( $args['activity_id'] ) ) {
		if ( ( empty( $args['post_id'] ) ) || ( empty( $args['activity_type'] ) ) ) {
			return;
		}
	}

	if ( empty( $args['user_id'] ) ) {
		// If we don't have a user_id passed via args.
		if ( ! is_user_logged_in() ) {
			return; // If not logged in, abort.
		}

		// Else use the logged in user ID as the args user_id.
		$args['user_id'] = get_current_user_id();
	}

	// End of args processing. Finally after we have applied all the logic we go out for filters.
	/**
	 * Filters user activity arguments.
	 *
	 * @param array $args An array of user activity arguments.
	 */
	$args = apply_filters( 'learndash_update_user_activity_args', $args );
	if ( empty( $args ) ) {
		return;
	}

	$values_array = array(
		'user_id'       => $args['user_id'],
		'course_id'     => $args['course_id'],
		'post_id'       => $args['post_id'],
		'activity_type' => $args['activity_type'],
	);

	$types_array = array(
		'%d', // user_id.
		'%d', // course_id.
		'%d', // post_id.
		'%s', // activity_type.
	);

	if ( ( true === (bool) $args['activity_status'] ) || ( false === (bool) $args['activity_status'] ) ) {
		$values_array['activity_status'] = $args['activity_status'];
		$types_array[]                   = '%d';
	}

	if ( '' !== $args['activity_completed'] ) {
		$values_array['activity_completed'] = $args['activity_completed'];
		$types_array[]                      = '%d';
	}

	if ( '' !== $args['activity_started'] ) {
		$values_array['activity_started'] = $args['activity_started'];
		$types_array[]                    = '%d';
	}

	if ( '' !== $args['activity_updated'] ) {
		$values_array['activity_updated'] = $args['activity_updated'];
		$types_array[]                    = '%d';
	} else {
		if ( ( empty( $args['activity_started'] ) ) && ( empty( $args['activity_completed'] ) ) ) {
			if ( ! isset( $args['data_upgrade'] ) ) {
				$values_array['activity_updated'] = time();
				$types_array[]                    = '%d';
			}
		} elseif ( $args['activity_started'] == $args['activity_completed'] ) {
			$values_array['activity_updated'] = $args['activity_completed'];
			$types_array[]                    = '%d';
		} else {
			if ( $args['activity_started'] > $args['activity_completed'] ) {
				$values_array['activity_updated'] = $args['activity_started'];
				$types_array[]                    = '%d';
			} elseif ( $args['activity_completed'] > $args['activity_started'] ) {
				$values_array['activity_updated'] = $args['activity_completed'];
				$types_array[]                    = '%d';
			}
		}
	}

	$update_ret = false;

	if ( 'update' === $args['activity_action'] ) {

		if ( empty( $args['activity_id'] ) ) {
			$activity = learndash_get_user_activity( $args );
			if ( null !== $activity ) {

				$args['activity_id'] = $activity->activity_id;
			}
		}

		if ( ! empty( $args['activity_id'] ) ) {

			$update_values_array = $values_array;
			$update_types_array  = $types_array;

			$update_ret = $wpdb->update(
				LDLMS_DB::get_table_name( 'user_activity' ),
				$update_values_array,
				array(
					'activity_id' => $args['activity_id'],
				),
				$update_types_array,
				array(
					'%d', // activity_id.
				)
			);

		} else {
			$args['activity_action'] = 'insert';
		}
	}

	if ( 'insert' === $args['activity_action'] ) {

		$values_array['activity_updated'] = time();
		$types_array[]                    = '%d';

		$insert_ret = $wpdb->insert(
			LDLMS_DB::get_table_name( 'user_activity' ),
			$values_array,
			$types_array
		);

		if ( false !== (bool) $insert_ret ) {
			$args['activity_id'] = $wpdb->insert_id;
		}
	}

	// Finally for the course we update the activity meta.
	if ( ( ! empty( $args['activity_id'] ) ) && ( ! empty( $args['activity_meta'] ) ) ) {
		foreach ( $args['activity_meta'] as $meta_key => $meta_value ) {
			learndash_update_user_activity_meta( $args['activity_id'], $meta_key, $meta_value );
		}
	}

	/**
	 * Fires after updating user activity.
	 *
	 * @param array $args An array of activity arguments.
	 */
	do_action( 'learndash_update_user_activity', $args );

	return $args['activity_id'];
}

/**
 * Gets the user activity.
 *
 * @since 2.3.0
 * @since 3.5.0 Added `$activity_create` param.
 *
 * @global wpdb $wpdb WordPress database abstraction object.
 *
 * @param array $args {
 *    An array of user activity arguments. Default empty array.
 *
 *    @type int    $course_id     Optional. Course ID. Default 0.
 *    @type string $activity_type Type of the activity.
 * }
 * @param bool  $activity_create It true will create to activity record.
 *
 * @return object|array|null Activity object (LDLMS_Activity) or null if not found.
 */
function learndash_get_user_activity( $args = array(), $activity_create = false ) {
	global $wpdb;

	if ( ! isset( $args['course_id'] ) ) {
		$args['course_id'] = 0;
	}

	if ( ( ! isset( $args['activity_type'] ) ) || ( empty( $args['activity_type'] ) ) ) {
		if ( ( isset( $args['post_id'] ) ) && ( ! empty( $args['post_id'] ) ) ) {
			$post_type = get_post_type( $args['post_id'] );
			if ( learndash_is_valid_post_type( $post_type ) ) {
				$args['activity_type'] = learndash_get_post_type_key( $post_type );
			}
		}
	}

	if ( 'quiz' === $args['activity_type'] ) {
		$data_settings_quizzes = learndash_data_upgrades_setting( 'user-meta-quizzes' );
		if ( ! isset( $args['activity_completed'] ) ) {
			$args['activity_completed'] = 0;
		}
		if ( version_compare( $data_settings_quizzes['version'], '2.5', '>=' ) ) {
			$sql_str = $wpdb->prepare( 'SELECT * FROM ' . esc_sql( LDLMS_DB::get_table_name( 'user_activity' ) ) . ' WHERE user_id=%d AND course_id=%d AND post_id=%d AND activity_type=%s AND activity_completed=%d LIMIT 1', $args['user_id'], $args['course_id'], $args['post_id'], $args['activity_type'], $args['activity_completed'] );
		} else {
			$sql_str = $wpdb->prepare( 'SELECT * FROM ' . esc_sql( LDLMS_DB::get_table_name( 'user_activity' ) ) . ' WHERE user_id=%d AND post_id=%d AND activity_type=%s AND activity_completed=%d LIMIT 1', $args['user_id'], $args['post_id'], $args['activity_type'], $args['activity_completed'] );
		}
	} else {
		$data_settings_courses = learndash_data_upgrades_setting( 'user-meta-courses' );
		if ( version_compare( $data_settings_courses['version'], '2.5', '>=' ) ) {
			$sql_str = $wpdb->prepare( 'SELECT * FROM ' . esc_sql( LDLMS_DB::get_table_name( 'user_activity' ) ) . ' WHERE user_id=%d AND course_id=%d AND post_id=%d AND activity_type=%s LIMIT 1', $args['user_id'], $args['course_id'], $args['post_id'], $args['activity_type'] );
		} else {
			$sql_str = $wpdb->prepare( 'SELECT * FROM ' . esc_sql( LDLMS_DB::get_table_name( 'user_activity' ) ) . ' WHERE user_id=%d AND post_id=%d AND activity_type=%s LIMIT 1', $args['user_id'], $args['post_id'], $args['activity_type'] );
		}
	}
	// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- $sql_str prepared in previous lines
	$activity = $wpdb->get_row( $sql_str ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared
	if ( ! $activity ) {
		if ( true === $activity_create ) {
			$activity_id = learndash_update_user_activity( $args );
			if ( ! empty( $activity_id ) ) {
				$activity = $wpdb->get_row( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
					$wpdb->prepare(
						'SELECT * FROM ' . esc_sql( LDLMS_DB::get_table_name( 'user_activity' ) ) . ' WHERE activity_id=%d',
						$activity_id
					)
				);
			}
		}
	}

	if ( $activity ) {
		$activity = new LDLMS_Model_Activity( $activity );
	}

	/**
	 * Filter for learndash_get_user_activity.
	 *
	 * @since 3.2.3
	 *
	 * @param array $activity Object (LDLMS_Activity) of activity.
	 * @param array $args     Array of args used for activity query.
	 */
	$activity = apply_filters( 'learndash_get_user_activity', $activity, $args );

	return $activity;

}


/**
 * Gets the user activity meta.
 *
 * @since 2.3.0
 * @since 3.5.0 Added $return_activity_meta_value_unserialized param.
 *
 * @global wpdb $wpdb WordPress database abstraction object.
 *
 * @param int     $activity_id                             Optional. Activity ID. Default 0.
 * @param string  $activity_meta_key                       Optional. The activity meta key to get. Default empty.
 * @param boolean $return_activity_meta_value_only         Optional. Whether to return only activity meta value. Default true.
 * @param boolean $return_activity_meta_value_unserialized Optional. Whether to unserialize the single meta value.
 *
 * @return object Activity meta object.
 */
function learndash_get_user_activity_meta( $activity_id = 0, $activity_meta_key = '', $return_activity_meta_value_only = true, $return_activity_meta_value_unserialized = false ) {

	global $wpdb;

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

	if ( ! empty( $activity_meta_key ) ) {
		$activity_meta = $wpdb->get_row(
			$wpdb->prepare(
				'SELECT * FROM ' . esc_sql( LDLMS_DB::get_table_name( 'user_activity_meta' ) ) . ' WHERE activity_id=%d AND activity_meta_key=%s',
				$activity_id,
				$activity_meta_key
			)
		);
		if ( ! empty( $activity_meta ) ) {
			if ( true === (bool) $return_activity_meta_value_only ) {
				if ( property_exists( $activity_meta, 'activity_meta_value' ) ) {
					if ( true === (bool) $return_activity_meta_value_unserialized ) {
						return maybe_unserialize( $activity_meta->activity_meta_value );
					} else {
						return $activity_meta->activity_meta_value;
					}
				}
			}
		}
		return $activity_meta;
	} else {
		// Here we return ALL meta for the given activity_id.
		return $wpdb->get_results(
			$wpdb->prepare(
				'SELECT * FROM ' . esc_sql( LDLMS_DB::get_table_name( 'user_activity_meta' ) ) . ' WHERE activity_id=%d',
				$activity_id
			)
		);
	}
}

/**
 * Updates the user activity meta.
 *
 * @since 2.3.0
 *
 * @global wpdb $wpdb WordPress database abstraction object.
 *
 * @param int                 $activity_id Optional. Activity ID. Default 0.
 * @param string              $meta_key    Optional. The activity meta key to get. Default empty.
 * @param string|mixed[]|null $meta_value  Optional. Activity meta value. Default null.
 */
function learndash_update_user_activity_meta( $activity_id = 0, $meta_key = '', $meta_value = null ) {
	global $wpdb;

	if ( ( empty( $activity_id ) ) || ( empty( $meta_key ) ) || ( null === $meta_value ) ) {
		return;
	}

	$activity = learndash_get_user_activity_meta( $activity_id, $meta_key, false );
	if ( null !== $activity ) {
		$wpdb->update(
			LDLMS_DB::get_table_name( 'user_activity_meta' ),
			array(
				'activity_id'         => $activity_id,
				'activity_meta_key'   => $meta_key,
				'activity_meta_value' => maybe_serialize( $meta_value ),
			),
			array(
				'activity_meta_id' => $activity->activity_meta_id,
			),
			array(
				'%d',   // activity_id.
				'%s',   // meta_key.
				'%s',   // meta_value.
			),
			array(
				'%d',   // activity_meta_id.
			)
		);

	} else {
		$wpdb->insert(
			LDLMS_DB::get_table_name( 'user_activity_meta' ),
			array(
				'activity_id'         => $activity_id,
				'activity_meta_key'   => $meta_key,
				'activity_meta_value' => maybe_serialize( $meta_value ),
			),
			array(
				'%d',   // activity_id.
				'%s',   // meta_key.
				'%s',   // meta_value.
			)
		);
	}
}

/**
 * Delete the user activity meta.
 *
 * @since 3.5.0
 *
 * @global wpdb $wpdb WordPress database abstraction object.
 *
 * @param int    $activity_id Optional. Activity ID. Default 0.
 * @param string $meta_key    Optional. The activity meta key to delete. If empty will delete all.
 */
function learndash_delete_user_activity_meta( $activity_id = 0, $meta_key = '' ) {
	global $wpdb;

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

	$activity = learndash_get_user_activity_meta( $activity_id, $meta_key, false );
	if ( null !== $activity ) {
		if ( is_object( $activity ) ) {
			$activity = array( $activity );
		}

		foreach ( $activity as $activity_item ) {
			$wpdb->delete(
				LDLMS_DB::get_table_name( 'user_activity_meta' ),
				array(
					'activity_meta_id' => $activity_item->activity_meta_id,
				),
				array(
					'%d',   // activity_meta_id.
				)
			);
		}
	}
}

/**
 * Deletes the user activity.
 *
 * @since 2.5.0
 *
 * @global wpdb $wpdb WordPress database abstraction object.
 *
 * @param int $activity_id Optional. Activity ID. Default 0.
 */
function learndash_delete_user_activity( $activity_id = 0 ) {
	global $wpdb;

	if ( ! empty( $activity_id ) ) {
		$wpdb->delete(
			LDLMS_DB::get_table_name( 'user_activity' ),
			array( 'activity_id' => $activity_id ),
			array( '%d' )
		);

		$wpdb->delete(
			LDLMS_DB::get_table_name( 'user_activity_meta' ),
			array( 'activity_id' => $activity_id ),
			array( '%d' )
		);
	}
}

/**
 * Get the earliest course activity record for the user.
 *
 * @since 3.5.0
 *
 * @param int $user_id          User ID.
 * @param int $course_id        Course ID.
 * @param int $default_started  Default value.
 */
function learndash_activity_course_get_earliest_started( $user_id = 0, $course_id = 0, $default_started = 0 ) {
	$user_id   = absint( $user_id );
	$course_id = absint( $course_id );

	if ( ( ! empty( $user_id ) ) && ( ! empty( $course_id ) ) ) {
		$user_activity = learndash_user_get_course_progress( $user_id, $course_id, 'activity' );
		if ( ! empty( $user_activity ) ) {
			if ( isset( $user_activity[ learndash_get_post_type_slug( 'course' ) . ':' . $course_id ] ) ) {
				unset( $user_activity[ learndash_get_post_type_slug( 'course' ) . ':' . $course_id ] );
			}
			$activity_started = wp_list_pluck( $user_activity, 'activity_started' );
			$activity_started = array_map( 'absint', $activity_started ); // Give me only integers.
			$activity_started = array_diff( $activity_started, array( 0 ) ); // Remove empty values.
			if ( ! empty( $activity_started ) ) {
				sort( $activity_started );
				$default_started = (int) $activity_started[0];
			}
		}
	}

	return $default_started;
}

/**
 * Set the step activity started record.
 *
 * @since 3.5.0
 *
 * @param int    $user_id    User ID.
 * @param int    $course_id  Course ID.
 * @param int    $step_id    Course step ID.
 * @param string $type       Step type 'course', lesson', 'topic', 'quiz.
 * @param int    $start_time Activity start timestamp (GMT).
 *
 * @return object Instance of LDLMS_Model_Activity or null;
 */
function learndash_activity_start_step( $user_id = 0, $course_id = 0, $step_id = 0, $type = '', $start_time = 0 ) {
	if ( empty( $start_time ) ) {
		$start_time = time();
	}
	$args = array(
		'course_id'        => $course_id,
		'user_id'          => $user_id,
		'post_id'          => $step_id,
		'activity_type'    => $type,
		'activity_started' => $start_time,
	);
	return learndash_get_user_activity( $args, true );
}

/**
 * Set the step activity completed record.
 *
 * @since 3.5.0
 *
 * @param int    $user_id       User ID.
 * @param int    $course_id     Course ID.
 * @param int    $step_id       Course step ID.
 * @param string $type          Step type 'course', lesson', 'topic', 'quiz.
 * @param int    $complete_time Activity complete timestamp (GMT).
 *
 * @return object Instance of LDLMS_Model_Activity or null;
 */
function learndash_activity_complete_step( $user_id = 0, $course_id = 0, $step_id = 0, $type = '', $complete_time = 0 ) {
	if ( empty( $complete_time ) ) {
		$complete_time = time();
	}
	$args     = array(
		'course_id'     => $course_id,
		'user_id'       => $user_id,
		'post_id'       => $step_id,
		'activity_type' => $type,
	);
	$activity = learndash_get_user_activity( $args, true );
	if ( ! empty( $activity ) ) {
		$activity = json_decode( wp_json_encode( $activity ), true );
	} else {
		$activity = $args;
	}
	$activity['activity_status']    = true;
	$activity['activity_completed'] = $complete_time;
	$activity['activity_updated']   = $complete_time;

	if ( empty( $activity['activity_started'] ) ) {
		$activity['activity_started'] = $complete_time;
	}

	$activity_id = learndash_update_user_activity( $activity );
	if ( ! empty( $activity_id ) ) {
		return learndash_get_user_activity( $activity, true );
	}
	return null;
}

/**
 * Set the course activity started record.
 *
 * @since 3.5.0
 *
 * @param int  $user_id          User ID.
 * @param int  $course_id        Course ID.
 * @param int  $start_time       Activity start timestamp (GMT).
 * @param bool $force_start_time Force update on start time.
 *
 * @return object Instance of LDLMS_Model_Activity or null;
 */
function learndash_activity_start_course( $user_id = 0, $course_id = 0, $start_time = 0, $force_start_time = true ) {
	$activity = learndash_activity_start_step( $user_id, $course_id, $course_id, 'course', $start_time );
	if ( $activity ) {

		if ( ( ! empty( absint( $start_time ) ) ) && ( empty( absint( $activity->activity_started ) ) ) ) {
			$activity->activity_started = absint( $start_time );
			learndash_update_user_activity( $activity );
		}
	}

	return $activity;
}

/**
 * Set the lesson activity started record.
 *
 * @since 3.5.0
 *
 * @param int $user_id    User ID.
 * @param int $course_id  Course ID.
 * @param int $lesson_id  Lesson ID.
 * @param int $start_time Activity start timestamp (GMT).
 *
 * @return object Instance of LDLMS_Model_Activity or null;
 */
function learndash_activity_start_lesson( $user_id = 0, $course_id = 0, $lesson_id = 0, $start_time = 0 ) {
	return learndash_activity_start_step( $user_id, $course_id, $lesson_id, 'lesson', $start_time );
}

/**
 * Set the lesson activity completed record.
 *
 * @since 3.5.0
 *
 * @param int $user_id       User ID.
 * @param int $course_id     Course ID.
 * @param int $lesson_id     Lesson ID.
 * @param int $complete_time Activity complete timestamp (GMT).
 *
 * @return object Instance of LDLMS_Model_Activity or null;
 */
function learndash_activity_complete_lesson( $user_id = 0, $course_id = 0, $lesson_id = 0, $complete_time = 0 ) {
	return learndash_activity_complete_step( $user_id, $course_id, $lesson_id, 'lesson', $complete_time );
}

/**
 * Set the topic activity started record.
 *
 * @since 3.5.0
 *
 * @param int $user_id    User ID.
 * @param int $course_id  Course ID.
 * @param int $topic_id   Topic ID.
 * @param int $start_time Activity start timestamp (GMT).
 *
 * @return object Instance of LDLMS_Model_Activity or null;
 */
function learndash_activity_start_topic( $user_id = 0, $course_id = 0, $topic_id = 0, $start_time = 0 ) {
	return learndash_activity_start_step( $user_id, $course_id, $topic_id, 'topic', $start_time );
}

/**
 * Set the topic activity completed record.
 *
 * @since 3.5.0
 *
 * @param int $user_id       User ID.
 * @param int $course_id     Course ID.
 * @param int $topic_id      Topic ID.
 * @param int $complete_time Activity complete timestamp (GMT).
 *
 * @return object Instance of LDLMS_Model_Activity or null;
 */
function learndash_activity_complete_topic( $user_id = 0, $course_id = 0, $topic_id = 0, $complete_time = 0 ) {
	return learndash_activity_complete_step( $user_id, $course_id, $topic_id, 'topic', $complete_time );
}

/**
 * Set the lesson activity started record.
 *
 * @since 3.5.0
 *
 * @param int $user_id    User ID.
 * @param int $course_id  Course ID.
 * @param int $quiz_id    Quiz ID.
 * @param int $start_time Activity start timestamp (GMT).
 *
 * @return object Instance of LDLMS_Model_Activity or null;
 */
function learndash_activity_start_quiz( $user_id = 0, $course_id = 0, $quiz_id = 0, $start_time = 0 ) {
	return learndash_activity_start_step( $user_id, $course_id, $quiz_id, 'quiz', $start_time );
}

/**
 * Update activity meta set.
 *
 * This will update an array of activity meta record(s).
 *
 * @since 3.5.0
 *
 * @param int   $activity_id   Activity ID.
 * @param array $activity_meta Array of key/value meta data.
 */
function learndash_activity_update_meta_set( $activity_id = 0, $activity_meta = array() ) {
	if ( ( ! empty( $activity_id ) ) && ( is_array( $activity_meta ) ) ) {
		foreach ( $activity_meta as $meta_key => $meta_value ) {
			learndash_update_user_activity_meta( $activity_id, $meta_key, $meta_value );
		}
	}
}

/**
 * Get the latest completed course activity record for the user.
 *
 * @since 3.5.0
 *
 * @param int $user_id   User ID.
 * @param int $course_id Course ID.
 */
function learndash_activity_course_get_latest_completed_step( $user_id = 0, $course_id = 0 ) {
	global $wpdb;

	$user_id   = absint( $user_id );
	$course_id = absint( $course_id );

	if ( ( ! empty( $user_id ) ) && ( ! empty( $course_id ) ) ) {
		$activity_item = $wpdb->get_row(
			$wpdb->prepare( 'SELECT post_id, activity_completed FROM ' . esc_sql( LDLMS_DB::get_table_name( 'user_activity' ) ) . ' WHERE course_id=%d AND user_id=%d AND activity_type IN ( "lesson", "topic", "quiz" ) AND activity_completed > 0 ORDER BY activity_completed DESC LIMIT 1', $course_id, $user_id ),
			ARRAY_A
		);

		return $activity_item;
	}
}

/**
 * Bulk creates course access activity records for user/course combinations.
 *
 * Handles efficient creation of 'access' type activity records:
 * 1. Queries existing records to avoid duplicates
 * 2. Filters to find only missing combinations
 * 3. Bulk inserts in chunks for efficiency
 *
 * @since 5.0.1
 *
 * @param int[] $user_ids   Array of user IDs.
 * @param int[] $course_ids Array of course IDs.
 * @param int   $group_id   Optional. Group ID for looking up accurate enrollment timestamps.
 *                          When provided, uses the later of user-group or course-group enrollment dates.
 *                          When omitted, uses current time.
 *
 * @return int Number of activity records created.
 */
function learndash_bulk_create_course_access_activities( array $user_ids, array $course_ids, int $group_id = 0 ): int {
	if (
		/**
		 * Filters whether to skip bulk creation of course access activities.
		 *
		 * @since 5.0.1
		 *
		 * @param bool  $skip Whether to skip bulk creation of course access activities. Default false.
		 * @param int[] $user_ids Array of user IDs.
		 * @param int[] $course_ids Array of course IDs.
		 * @param int   $group_id Group ID.
		 *
		 * @return bool Whether to skip bulk creation of course access activities. Default false.
		 */
		apply_filters( 'learndash_bulk_create_course_access_activities_skip', false, $user_ids, $course_ids, $group_id )
		|| empty( $user_ids )
		|| empty( $course_ids )
	) {
		return 0;
	}

	$activity_table = LDLMS_DB::get_table_name( 'user_activity' );

	// Query existing 'access' activity records to avoid duplicates.
	$existing_records = DB::table( DB::raw( $activity_table ) )
		->select( 'user_id', 'course_id' )
		->whereIn( 'user_id', $user_ids )
		->whereIn( 'course_id', $course_ids )
		->where( 'activity_type', 'access' )
		->getAll();

	if ( ! is_array( $existing_records ) ) {
		$existing_records = [];
	}

	// Build a hash set of existing combinations for O(1) lookup.
	$existing_set = [];
	foreach ( $existing_records as $record ) {
		$existing_set[ $record->user_id . '_' . $record->course_id ] = true;
	}

	// Find combinations that need new activity records.
	$current_time = time();
	$new_records  = [];

	// Pre-fetch course-group enrollment timestamps if group_id is provided.
	$course_group_times = [];
	if ( $group_id > 0 ) {
		foreach ( $course_ids as $course_id ) {
			$course_group_times[ $course_id ] = (int) learndash_group_course_access_from( $group_id, $course_id );
		}
	}

	foreach ( $user_ids as $user_id ) {
		// Get user-group enrollment timestamp if group_id is provided.
		$user_group_time = 0;
		if ( $group_id > 0 ) {
			$user_group_time = (int) learndash_group_access_from( $group_id, $user_id );
		}

		foreach ( $course_ids as $course_id ) {
			$key = $user_id . '_' . $course_id;

			if ( isset( $existing_set[ $key ] ) ) {
				continue;
			}

			// Determine the access timestamp.
			// When group_id is provided, use the later of user-group or course-group enrollment.
			// This represents when both conditions were met for the user to access the course.
			$access_time = $current_time;
			if ( $group_id > 0 ) {
				$course_time = $course_group_times[ $course_id ] ?? 0;
				if (
					$user_group_time > 0
					&& $course_time > 0
				) {
					$access_time = max( $user_group_time, $course_time );
				} elseif ( $user_group_time > 0 ) {
					$access_time = $user_group_time;
				} elseif ( $course_time > 0 ) {
					$access_time = $course_time;
				}
			}

			$new_records[] = [
				'user_id'          => $user_id,
				'course_id'        => $course_id,
				'post_id'          => $course_id,
				'activity_type'    => 'access',
				'activity_started' => $access_time,
				'activity_status'  => 0,
				'activity_updated' => $access_time,
			];
		}
	}

	if ( empty( $new_records ) ) {
		return 0;
	}

	/**
	 * Filters the chunk size for bulk course access activity creation.
	 *
	 * @since 5.0.1
	 *
	 * @param int $chunk_size The number of records to insert per database query. Default 500.
	 *
	 * @return int The number of records to insert per database query. Default 500.
	 */
	$chunk_size    = max( 1, Cast::to_int( apply_filters( 'learndash_group_access_activity_chunk_size', 500 ) ) );
	$created_count = 0;

	foreach ( array_chunk( $new_records, $chunk_size ) as $chunk ) {
		$values       = [];
		$placeholders = [];

		foreach ( $chunk as $record ) {
			$placeholders[] = '(%d, %d, %d, %s, %d, %d, %d)';
			$values[]       = $record['user_id'];
			$values[]       = $record['course_id'];
			$values[]       = $record['post_id'];
			$values[]       = $record['activity_type'];
			$values[]       = $record['activity_started'];
			$values[]       = $record['activity_status'];
			$values[]       = $record['activity_updated'];
		}

		// Bulk insert. QueryBuilder::insert() cannot be used as wpdb::insert() only can insert one row at a time.
		$sql = sprintf(
			'INSERT INTO %s (user_id, course_id, post_id, activity_type, activity_started, activity_status, activity_updated) VALUES %s',
			$activity_table,
			implode( ', ', $placeholders )
		);

		// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- $sql is built with placeholders above.
		$prepared_sql = Cast::to_string( DB::prepare( $sql, ...$values ) );

		if ( empty( $prepared_sql ) ) {
			continue;
		}

		// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Prepared above, bulk insert requires direct query, no caching needed for inserts.
		$inserted = DB::query( $prepared_sql );

		if ( false !== $inserted ) {
			$created_count += count( $chunk );
		}
	}

	return $created_count;
}
