<?php
/**
 * LearnDash Sensor helper.
 *
 * @package wsal
 * @subpackage sensors
 *
 * @since 5.6.0
 */

declare(strict_types=1);

namespace WSAL\WP_Sensors\Helpers;

use WSAL\Helpers\WP_Helper;

// Exit if accessed directly.
if ( ! \defined( 'ABSPATH' ) ) {
	exit;
}

if ( ! \class_exists( '\WSAL\WP_Sensors\Helpers\LearnDash_Helper' ) ) {
	/**
	 * Helper Sensor class for LearnDash.
	 *
	 * @package wsal
	 * @subpackage sensors-helpers
	 *
	 * @since 5.6.0
	 */
	class LearnDash_Helper {

		/**
		 * Stores if the plugin is active.
		 *
		 * @var bool
		 *
		 * @since 5.6.0
		 */
		private static $plugin_active = null;

		/**
		 * Stores if the plugin is active for sensors.
		 *
		 * @var bool
		 *
		 * @since 5.6.0
		 */
		private static $plugin_active_for_sensors = null;

		/**
		 * Check if LearnDash is active.
		 *
		 * @return bool
		 *
		 * @since 5.6.0
		 */
		public static function is_learndash_active() {
			if ( null === self::$plugin_active ) {
				if ( \function_exists( 'learndash_get_post_type_slug' ) ) {
					self::$plugin_active = true;
				} else {
					self::$plugin_active = false;
				}
			}

			return self::$plugin_active;
		}

		/**
		 * Check if plugin alerts should be loaded.
		 *
		 * @return boolean
		 *
		 * @since 5.6.0
		 */
		public static function load_alerts_for_sensor(): bool {
			if ( null === self::$plugin_active_for_sensors ) {
				self::$plugin_active_for_sensors = ( WP_Helper::is_plugin_active( 'sfwd-lms/sfwd_lms.php' ) );
			}

			return self::$plugin_active_for_sensors;
		}

		/**
		 * Register custom event objects for WSAL.
		 *
		 * @param array $objects - Array of objects current registered within WSAL.
		 *
		 * @return array
		 *
		 * @since 5.6.0
		 */
		public static function wsal_learndash_add_custom_event_objects( $objects ) {
			$new_objects = array(
				'learndash_courses'      => \esc_html__( 'LearnDash Courses', 'wp-security-audit-log' ),
				'learndash_lessons'      => \esc_html__( 'LearnDash Lessons', 'wp-security-audit-log' ),
				'learndash_topics'       => \esc_html__( 'LearnDash Topics', 'wp-security-audit-log' ),
				'learndash_quizzes'      => \esc_html__( 'LearnDash Quizzes', 'wp-security-audit-log' ),
				'learndash_groups'       => \esc_html__( 'LearnDash Groups', 'wp-security-audit-log' ),
				'learndash_settings'     => \esc_html__( 'LearnDash Settings', 'wp-security-audit-log' ),
				'learndash_certificates' => \esc_html__( 'LearnDash Certificates', 'wp-security-audit-log' ),
				'learndash_students'     => \esc_html__( 'LearnDash Students', 'wp-security-audit-log' ),
			);

			// combine the two arrays.
			$objects = \array_merge( $objects, $new_objects );

			return $objects;
		}

		/**
		 * Returns a list of custom post types associated with LearnDash.
		 *
		 * @return array - List of custom post types.
		 *
		 * @since 5.6.0
		 */
		public static function get_custom_post_types(): array {
			return array( 'sfwd-courses', 'sfwd-lessons', 'sfwd-topic', 'sfwd-quiz', 'groups', 'sfwd-question', 'sfwd-certificates' );
		}

		/**
		 * Returns a list of custom taxonomies associated with LearnDash.
		 *
		 * @return array - List of custom taxonomies.
		 *
		 * @since 5.6.0
		 */
		public static function get_custom_taxonomies(): array {
			return array( 'ld_course_category', 'ld_course_tag', 'ld_lesson_category', 'ld_lesson_tag', 'ld_topic_category', 'ld_topic_tag', 'ld_quiz_category', 'ld_group_category', 'ld_group_tag' );
		}

		/**
		 * Ignore Learndash CPTs for the WP Content sensors.
		 *
		 * @param array $post_types - An array of default post_types.
		 *
		 * @return array
		 *
		 * @since 5.6.0
		 */
		public static function wsal_learndash_add_custom_ignored_cpt( $post_types ) {
			$new_post_types = self::get_custom_post_types();

			// combine the two arrays.
			$post_types = \array_merge( $post_types, $new_post_types );

			return $post_types;
		}

		/**
		 * Gets the filename of the plugin this extension is targeting.
		 *
		 * @return string
		 *
		 * @since 5.6.0
		 */
		public static function get_plugin_filename(): string {
			return 'sfwd-lms/sfwd_lms.php';
		}

		/**
		 * Get the author name from a post.
		 *
		 * @param \WP_Post $post - Post object.
		 *
		 * @return string
		 *
		 * @since 5.6.0
		 */
		public static function get_author_name( $post ): string {
			$post_author = \get_userdata( (int) $post->post_author );
			return $post_author ? $post_author->user_login : self::get_ld_unknown_string();
		}

		/**
		 * Get the author display name from a post.
		 *
		 * @param \WP_Post $post - Post object.
		 *
		 * @return string
		 *
		 * @since 5.6.0
		 */
		public static function get_author_display_name( $post ): string {
			$author_id           = (int) $post->post_author;
			$author_data         = \get_userdata( $author_id );
			$author_display_name = $author_data ? $author_data->display_name : self::get_ld_unknown_string();

			return $author_display_name;
		}


		/**
		 * Get WordPress categories as comma-separated string.
		 *
		 * @param int $post_id - Post ID.
		 *
		 * @return string
		 *
		 * @since 5.6.0
		 */
		public static function get_post_categories( $post_id ): string {
			$categories = \wp_get_post_terms( $post_id, 'category', array( 'fields' => 'names' ) );
			return ! empty( $categories ) && ! \is_wp_error( $categories ) ? \implode( ', ', $categories ) : self::get_ld_none_string();
		}

		/**
		 * Get the price type from a Learndash post.
		 *
		 * Retrieves the LearnDash price type meta value (e.g., 'open', 'free', 'paynow', 'subscribe', 'closed').
		 *
		 * @param int $post_id - Post ID.
		 *
		 * @return string - The price type as string, e.g. 'free' or 'Not set' if not available.
		 *
		 * @since 5.6.0
		 */
		public static function get_ld_post_price_type( $post_id ) {
			return \get_post_meta( $post_id, '_ld_price_type', true ) ?? \esc_html__( 'Not set', 'wp-security-audit-log' );
		}

		/**
		 * Get LearnDash post categories as comma-separated string.
		 *
		 * @param int    $post_id - Post ID.
		 * @param string $post_type - Post type.
		 *
		 * @return string
		 *
		 * @since 5.6.0
		 */
		public static function get_ld_post_categories( $post_id, $post_type ): string {

			$taxonomy_key = 'ld_course_category';

			if ( 'sfwd-lessons' === $post_type ) {
				$taxonomy_key = 'ld_lesson_category';
			} elseif ( 'sfwd-topic' === $post_type ) {
				$taxonomy_key = 'ld_topic_category';
			} elseif ( 'sfwd-quiz' === $post_type ) {
				$taxonomy_key = 'ld_quiz_category';
			} elseif ( 'groups' === $post_type ) {
				$taxonomy_key = 'ld_group_category';
			}

			$categories = \wp_get_post_terms( $post_id, $taxonomy_key, array( 'fields' => 'names' ) );

			if ( ! empty( $categories ) && ! \is_wp_error( $categories ) ) {
				$categories = \implode( ', ', $categories );
			} else {
				$categories = self::get_ld_none_string();
			}

			return $categories;
		}

		/**
		 * Build post event variables.
		 *
		 * @param \WP_Post $post       - Post object.
		 *
		 * @return array
		 *
		 * @since 5.6.0
		 */
		public static function build_ld_post_event_variables( \WP_Post $post ): array {
			$variables = array(
				'PostTitle'      => \esc_html( $post->post_title ),
				'PostID'         => $post->ID,
				'PostAuthor'     => self::get_author_name( $post ),
				'Categories'     => self::get_post_categories( $post->ID ),
				'LdPostCategory' => self::get_ld_post_categories( $post->ID, $post->post_type ),
			);

			if ( in_array( $post->post_type, array( 'sfwd-courses', 'groups' ), true ) ) {
				$variables['PriceType'] = self::get_ld_post_price_type( $post->ID );
			}

			return $variables;
		}



		/**
		 * Get term names from term IDs.
		 *
		 * @param array  $term_ids - Array of term IDs.
		 * @param string $taxonomy - Taxonomy slug.
		 *
		 * @return string
		 *
		 * @since 5.6.0
		 */
		public static function get_term_names_from_ids( $term_ids, $taxonomy ): string {
			$term_names = array();

			foreach ( $term_ids as $term_id ) {
				$term = \get_term_by( 'term_taxonomy_id', $term_id, $taxonomy );
				if ( $term && ! \is_wp_error( $term ) ) {
					$term_names[] = $term->name;
				}
			}

			return ! empty( $term_names ) ? \implode( ', ', $term_names ) : '';
		}

		/**
		 * Format lessons, topics, and quizzes under ld_course_steps metadata for later comparison.
		 *
		 * @param int    $post_id - Post ID.
		 * @param string $meta_key - Meta key being updated.
		 *
		 * @return array The course steps metadata.
		 *
		 * @since 5.6.0
		 */
		public static function format_course_steps_metadata( $post_id, $meta_key ): array {
			$old_meta_record   = \get_post_meta( $post_id, $meta_key, true );
			$unserialized_meta = \maybe_unserialize( $old_meta_record );

			$lessons = array();
			$topics  = array();
			$quizzes = array();

			if ( \is_array( $unserialized_meta ) || \is_object( $unserialized_meta ) ) {
				$lessons = self::extract_learndash_items( $unserialized_meta, 'sfwd-lessons' );
				$topics  = self::extract_learndash_items( $unserialized_meta, 'sfwd-topic' );
				$quizzes = self::extract_learndash_items( $unserialized_meta, 'sfwd-quiz' );
			}

			$course_steps_meta = array(
				'lessons'      => array_keys( $lessons ),
				'topics'       => $topics,
				'quizzes'      => array_keys( $quizzes ),
				'raw_metadata' => $unserialized_meta,
			);

			return $course_steps_meta;
		}

		/**
		 * Extracts lesson or topic IDs from LearnDash course steps metadata.
		 *
		 * @param array  $data      - Unserialized course steps metadata (array/object).
		 * @param string $post_type - The post type to extract ('sfwd-lessons' or 'sfwd-topic').
		 *
		 * @return array - Array of IDs. For lessons: associative array (id => data), for topics: numeric array of IDs.
		 *
		 * @since 5.6.0
		 */
		public static function extract_learndash_items( $data, $post_type ): array {
			return self::search_nested_ld_structure( $data, $post_type, array() );
		}

		/**
		 * Recursively searches nested structure for LearnDash items.
		 *
		 * @param mixed  $node      - Current node being examined.
		 * @param string $post_type - The post type to extract.
		 * @param array  $items     - Accumulated items found so far.
		 *
		 * @return array - Updated items array.
		 *
		 * @since 5.6.0
		 */
		public static function search_nested_ld_structure( $node, $post_type, $items = array() ): array {

			// Return early if this isn't an array.
			if ( ! \is_array( $node ) ) {
				return $items;
			}

			foreach ( $node as $key => $value ) {
				if ( $post_type === $key && ( \is_array( $value ) || \is_object( $value ) ) ) {
					foreach ( (array) $value as $item_id => $item_data ) {
						if ( 'sfwd-lessons' === $post_type || 'sfwd-quiz' === $post_type ) {
							$items[ (string) $item_id ] = $item_data;
						} elseif ( 'sfwd-topic' === $post_type ) {
							$items[] = (int) $item_id;
						}
					}

					$items = self::search_nested_ld_structure( $value, $post_type, $items );
					continue;
				}

				if ( \is_array( $value ) || \is_object( $value ) ) {
					$items = self::search_nested_ld_structure( $value, $post_type, $items );
				}
			}

			return $items;
		}

		/**
		 * Get enrollment status label from course prerequisites or points settings.
		 *
		 * @param array $meta - Course meta data.
		 *
		 * @return string - Formatted enrollment requirement label.
		 *
		 * @since 5.6.0
		 */
		public static function get_enrollment_status_label( $meta ): string {

			$course_req   = $meta['sfwd-courses_requirements_for_enrollment'] ?? self::get_ld_none_string();
			$prereq_value = $meta['sfwd-courses_course_prerequisite'] ?? array();

			// If requirements are based on points.
			if ( 'course_points_enabled' === $course_req && isset( $meta['sfwd-courses_course_points_access'] ) ) {
				return \esc_html__( 'Points: ', 'wp-security-audit-log' ) . (string) $meta['sfwd-courses_course_points_access'];
			}

			// If requirements are based on prerequisite courses.
			if ( 'course_prerequisite_enabled' === $course_req ) {
				$course_titles = array();

				if ( is_array( $prereq_value ) ) {
					// Extract course titles from course post IDs.
					foreach ( $prereq_value as $prereq_id ) {

						$title = \get_the_title( (int) $prereq_id );

						if ( ! empty( $title ) ) {
							$course_titles[] = $title;
						}
					}

					if ( ! empty( $course_titles ) ) {
						return implode( ', ', $course_titles );
					}
				}
			}

			// Fallback to None if we did not find any requirements.
			return self::get_ld_none_string();
		}

		/**
		 * Formats duration in seconds to HH MM format.
		 *
		 * @param mixed $seconds - Duration in seconds.
		 *
		 * @return string - Formatted duration string.
		 *
		 * @since 5.6.0
		 */
		public static function format_duration_for_display( $seconds ): string {
			$seconds_string = (string) $seconds;

			if ( empty( $seconds_string ) ) {
				return self::get_ld_none_string();
			}

			$seconds_int = (int) $seconds;
			$hours       = (int) \floor( $seconds_int / 3600 );
			$minutes     = (int) \floor( ( $seconds_int % 3600 ) / 60 );

			$parts = array();

			if ( $hours > 0 ) {
				$parts[] = \sprintf(
				/* translators: %s: number of hours */
					\_n( '%s hour', '%s hours', $hours, 'wp-security-audit-log' ),
					\number_format_i18n( $hours )
				);
			}

			if ( $minutes > 0 ) {
				$parts[] = \sprintf(
				/* translators: %d: number of minutes */
					\_n( '%d minute', '%d minutes', $minutes, 'wp-security-audit-log' ),
					\number_format_i18n( $minutes )
				);
			}

			return implode( ', ', $parts );
		}

		/**
		 * Get taxonomy edit link.
		 *
		 * @param int    $term_id - Term ID.
		 * @param string $taxonomy - Taxonomy slug.
		 * @param string $post_type - Post type (optional). If provided, uses term.php format with post_type.
		 *
		 * @return string|null
		 *
		 * @since 5.6.0
		 */
		public static function get_ld_taxonomy_edit_link( $term_id, $taxonomy = 'post_tag', $post_type = '' ) {
			if ( empty( $term_id ) ) {
				return null;
			}

			if ( ! empty( $post_type ) ) {
				$term_args = array(
					'taxonomy'  => $taxonomy,
					'tag_ID'    => $term_id,
					'post_type' => $post_type,
				);

				return \add_query_arg( $term_args, \network_admin_url( 'term.php' ) );
			}

			// Use term.php format, standard WordPress format for taxonomy term list as fall back if we don't have a post type.
			$term_args = array(
				'taxonomy' => $taxonomy,
				'tag_ID'   => $term_id,
			);

			return \add_query_arg( $term_args, \network_admin_url( 'term.php' ) );
		}

		/**
		 * Build a map of quiz locations with their parent IDs across course/lesson/topic hierarchy.
		 *
		 * @param array $steps_data - The steps['h'] data structure as provided by Learndash 'ld_course_steps' metadata.
		 * @param int   $post_id - Course ID.
		 *
		 * @return array - Array with 'course_level', 'lesson_level', 'topic_level' keys, each containing arrays of quiz_id/parent_id pairs.
		 *
		 * @since 5.6.0
		 */
		public static function map_quizzes_by_level( $steps_data, $post_id ) {
			$quiz_map = array(
				'course_level' => array(),
				'lesson_level' => array(),
				'topic_level'  => array(),
			);

			// Course-level quizzes.
			if ( isset( $steps_data['sfwd-quiz'] ) && ! empty( $steps_data['sfwd-quiz'] ) ) {
				foreach ( array_keys( $steps_data['sfwd-quiz'] ) as $quiz_id ) {
					$quiz_map['course_level'][] = array(
						'quiz_id'   => $quiz_id,
						'parent_id' => $post_id,
					);
				}
			}

			// Lesson-level and topic-level quizzes.
			if ( isset( $steps_data['sfwd-lessons'] ) && ! empty( $steps_data['sfwd-lessons'] ) ) {
				foreach ( $steps_data['sfwd-lessons'] as $lesson_id => $lesson_data ) {
					// Lesson-level quizzes.
					if ( isset( $lesson_data['sfwd-quiz'] ) && ! empty( $lesson_data['sfwd-quiz'] ) ) {
						foreach ( array_keys( $lesson_data['sfwd-quiz'] ) as $quiz_id ) {
							$quiz_map['lesson_level'][] = array(
								'quiz_id'   => $quiz_id,
								'parent_id' => $lesson_id,
							);
						}
					}

					// Topic-level quizzes.
					if ( isset( $lesson_data['sfwd-topic'] ) && ! empty( $lesson_data['sfwd-topic'] ) ) {
						foreach ( $lesson_data['sfwd-topic'] as $topic_id => $topic_data ) {
							if ( isset( $topic_data['sfwd-quiz'] ) && ! empty( $topic_data['sfwd-quiz'] ) ) {
								foreach ( array_keys( $topic_data['sfwd-quiz'] ) as $quiz_id ) {
									$quiz_map['topic_level'][] = array(
										'quiz_id'   => $quiz_id,
										'parent_id' => $topic_id,
									);
								}
							}
						}
					}
				}
			}

			return $quiz_map;
		}

		/**
		 * Get post titles from an array of post IDs.
		 *
		 * @param array $post_ids - Array of post IDs.
		 *
		 * @return array - Array of post titles.
		 *
		 * @since 5.6.0
		 */
		public static function get_post_titles_array( array $post_ids ): array {
			$titles = array();

			$post_ids = \array_values( \array_filter( $post_ids ) );

			foreach ( $post_ids as $post_id ) {
				$titles[] = \get_the_title( $post_id );
			}

			return $titles;
		}

		/**
		 * Get all enrolled course IDs for a user from LearnDash enrollment meta.
		 *
		 * @param int $user_id - User ID.
		 *
		 * @return array - Array of course IDs.
		 *
		 * @since 5.6.0
		 */
		public static function get_user_enrolled_courses( $user_id ) {
			$all_meta   = \get_user_meta( $user_id );
			$course_ids = array();

			foreach ( $all_meta as $meta_key => $meta_value ) {
				if ( \preg_match( '/^course_(\d+)_access_from$/', $meta_key, $matches ) ) {
					$course_ids[] = (int) $matches[1];
				}
			}

			return $course_ids;
		}

		/**
		 * Get all enrolled group IDs for a user.
		 *
		 * @param int $user_id - User ID.
		 *
		 * @return array - Array of group IDs.
		 *
		 * @since 5.6.0
		 */
		public static function get_user_enrolled_groups( $user_id ) {
			if ( \function_exists( 'learndash_get_users_group_ids' ) ) {
				return \learndash_get_users_group_ids( $user_id );
			}

			return array();
		}

		/**
		 * Checks if a new post is a duplicate of a source post.
		 *
		 * @param \WP_Post $new_post       The new post object.
		 * @param int      $source_post_id The ID of the source post.
		 *
		 * @return bool True if it is a duplicate, false otherwise.
		 *
		 * @since 5.6.0
		 */
		public static function is_post_duplicate( $new_post, $source_post_id ) {
			$source_post = \get_post( $source_post_id );

			if ( ! $source_post || $source_post->post_type !== $new_post->post_type ) {
				return false;
			}

			// Compare content.
			if ( $new_post->post_content !== $source_post->post_content ) {
				return false;
			}

			return true;
		}

		/**
		 * Check if a post was just created (within the last few seconds).
		 *
		 * @param \WP_Post $post - Post object.
		 *
		 * @return bool - True if post was just created, false otherwise.
		 *
		 * @since 5.6.0
		 */
		public static function is_post_just_created( $post ) {
			if ( ! $post ) {
				return false;
			}

			// Use get_post_datetime to handle timezones and GMT/local fallback.
			$post_datetime = \get_post_datetime( $post, 'date', 'gmt' );

			if ( ! $post_datetime ) {
				return false;
			}

			$created_timestamp = $post_datetime->getTimestamp();
			$current_timestamp = \time();

			// For scheduled posts, use modified date as they are "created" in the future.
			if ( 'future' === $post->post_status ) {
				$mod_datetime = \get_post_datetime( $post, 'modified', 'gmt' );
				if ( $mod_datetime ) {
					$created_timestamp = $mod_datetime->getTimestamp();
				}
			}

			$age = $current_timestamp - $created_timestamp;

			// Check if created within last 30 seconds, let's also set -5 to allow for minor time differences.
			return $age >= -5 && $age < 30;
		}

		/**
		 * Format lesson order for display: titles if 10 or fewer lessons, IDs if more than 10.
		 *
		 * @param array $lesson_ids - Ordered array of lesson IDs.
		 *
		 * @return string - Formatted comma-separated string of lesson titles or IDs.
		 *
		 * @since 5.6.0
		 */
		public static function format_lesson_order_for_display( $lesson_ids ) {
			if ( empty( $lesson_ids ) ) {
				return '';
			}

			// If 10 or fewer lessons, show titles.
			if ( \count( $lesson_ids ) <= 10 ) {

				$titles = array();

				foreach ( $lesson_ids as $lesson_id ) {
					$titles[] = \get_the_title( $lesson_id );
				}
				return \implode( ', ', $titles );
			}

			// If more than 10 lessons, show IDs.
			return \implode( ', ', $lesson_ids );
		}

		/**
		 * Get translated 'None' string for LearnDash fields.
		 *
		 * @return string
		 *
		 * @since 5.6.0
		 */
		public static function get_ld_none_string() {
			return \esc_html__( 'None', 'wp-security-audit-log' );
		}

		/**
		 * Get translated 'Unknown' string for LearnDash fields.
		 *
		 * @return string
		 *
		 * @since 5.6.0
		 */
		public static function get_ld_unknown_string() {
			return \esc_html__( 'Unknown', 'wp-security-audit-log' );
		}

		/**
		 * Get the student count label.
		 *
		 * @param int $count - Student count.
		 *
		 * @return string
		 *
		 * @since 5.6.0
		 */
		public static function get_student_count_label( $count ) {
			/* translators: %d: Number of students */
			return $count > 0 ? sprintf( \_n( '%d student', '%d students', $count, 'wp-security-audit-log' ), $count ) : self::get_ld_none_string();
		}

		/**
		 * Format date to a valid float value.
		 *
		 * @param mixed $date_value - Date value.
		 *
		 * @return float
		 *
		 * @since 5.6.0
		 */
		public static function cast_date_to_float( $date_value ): float {
			if ( empty( $date_value ) || '0' === (string) $date_value ) {
				return 0;
			}

			return (float) $date_value;
		}

		/**
		 * Determine if a LearnDash post was created via the LearnDash Course Builder.
		 *
		 * LearnDash saves posts created via the course builder in 2 steps:
		 * 1. First, an empty post with a default name and slug (e.g., "lesson", "topic", "quiz").
		 * 2. Second, it updates the post with the name and slug specified by the user.
		 *
		 * This method detects the second step by checking if:
		 * - The post slug changed from the default builder slug to a user-specified slug.
		 * - The post author remained the same (it's impossible to change author from the course builder).
		 *
		 * @param \WP_Post $post_before - Post object before the update.
		 * @param \WP_Post $post_after  - Post object after the update.
		 *
		 * @return bool - True if post was created via the Course Builder, false otherwise.
		 *
		 * @since 5.6.0
		 */
		public static function ld_post_created_via_ld_course_builder( $post_before, $post_after ) {
			// List post types that can be created via the Learndash course builder.
			$ld_builder_post_types = array( 'sfwd-lessons', 'sfwd-topic', 'sfwd-quiz' );

			$default_post_title_from_builder = array(
				'sfwd-lessons' => 'Lesson',
				'sfwd-topic'   => 'Topic',
				'sfwd-quiz'    => 'Quiz',
			);

			// If not a valid post type, return false.
			if ( ! \in_array( $post_after->post_type, $ld_builder_post_types, true ) ) {
				return false;
			}

			// Return early if author is not the same.
			if ( (int) $post_before->post_author !== (int) $post_after->post_author ) {
				return false;
			}

			$post_type = $post_after->post_type;

			// Check if the post title changed from the default one set by the builder to the one specified by the user.
			if ( $default_post_title_from_builder[ $post_type ] === $post_before->post_title && $default_post_title_from_builder[ $post_type ] !== $post_after->post_title ) {
				return true;
			}

			return false;
		}


		/**
		 * Get Learndash start and end dates from post meta, for courses and groups.
		 *
		 * @param int    $post_id - Post ID.
		 * @param string $post_type - Post type ('sfwd-courses' or 'groups').
		 *
		 * @return array - Array with 'start_date' and 'end_date' keys.
		 *
		 * @since 5.6.0
		 */
		public static function get_ld_start_end_dates_meta( $post_id, $post_type ): array {

			$meta_key       = '';
			$start_date_key = '';
			$end_date_key   = '';

			$dates = array(
				'start_date' => null,
				'end_date'   => null,
			);

			// Return early if post type is not supported.
			if ( ! in_array( $post_type, array( 'sfwd-courses', 'groups' ), true ) ) {
				return $dates;
			}

			// Determine meta key and date field keys based on post type.
			if ( 'sfwd-courses' === $post_type ) {
				$meta_key       = '_sfwd-courses';
				$start_date_key = 'sfwd-courses_course_start_date';
				$end_date_key   = 'sfwd-courses_course_end_date';
			} elseif ( 'groups' === $post_type ) {
				$meta_key       = '_groups';
				$start_date_key = 'groups_group_start_date';
				$end_date_key   = 'groups_group_end_date';
			}

			// Get current dates from database (before any updates).
			$current_meta      = \get_post_meta( $post_id, $meta_key, true );
			$unserialized_meta = \maybe_unserialize( $current_meta );

			if ( \is_array( $unserialized_meta ) ) {
				$start_date = $unserialized_meta[ $start_date_key ] ?? null;
				$end_date   = $unserialized_meta[ $end_date_key ] ?? null;

				$dates = array(
					'start_date' => $start_date,
					'end_date'   => $end_date,
				);
			}

			return $dates;
		}

		/**
		 * Get a comma-separated string of course titles enrolled in a group.
		 *
		 * @param int $group_id - Group ID.
		 *
		 * @return string - Comma-separated course titles or 'None' if no courses.
		 *
		 * @since 5.6.0
		 */
		public static function get_ld_courses_string_by_group_id( $group_id ) {
			$available_courses = self::get_ld_none_string();

			if ( \function_exists( 'learndash_group_enrolled_courses' ) ) {
				$course_ids = \learndash_group_enrolled_courses( $group_id );

				if ( ! empty( $course_ids ) && \is_array( $course_ids ) ) {
					$course_titles     = self::get_post_titles_array( $course_ids );
					$available_courses = \implode( ', ', $course_titles );
				}
			}

			return $available_courses;
		}

		/**
		 * Get the parent lesson ID for a topic.
		 *
		 * @param int $topic_id - Topic post ID.
		 *
		 * @return int|null - Lesson ID or null if not found.
		 *
		 * @since 5.6.0
		 */
		public static function get_topic_lesson_id( $topic_id ) {
			if ( empty( $topic_id ) ) {
				return null;
			}

			// LearnDash stores the parent lesson ID in post meta.
			$lesson_id = \get_post_meta( $topic_id, 'lesson_id', true );

			if ( ! empty( $lesson_id ) ) {
				return (int) $lesson_id;
			}

			// Fallback: try using LearnDash function if available.
			if ( \function_exists( 'learndash_get_lesson_id' ) ) {
				$ld_lesson_id = \learndash_get_lesson_id( $topic_id );
				if ( ! empty( $ld_lesson_id ) ) {
					return (int) $ld_lesson_id;
				}
			}

			return null;
		}

		/**
		 * Get a comma-separated string of group titles for a course.
		 *
		 * @param int $course_id - Course ID.
		 *
		 * @return string - Comma-separated group titles or 'None' if no groups.
		 *
		 * @since 5.6.0
		 */
		public static function get_ld_post_groups_string( $course_id ) {
			if ( empty( $course_id ) ) {
				return self::get_ld_none_string();
			}

			$group_ids = array();

			// Get all groups that have this course enrolled.
			if ( \function_exists( 'learndash_get_course_groups' ) ) {
				$group_ids = \learndash_get_course_groups( $course_id, true );
			}

			if ( empty( $group_ids ) || ! \is_array( $group_ids ) ) {
				return self::get_ld_none_string();
			}

			$group_titles = self::get_post_titles_array( $group_ids );

			if ( ! empty( $group_titles ) ) {
				return \implode( ', ', $group_titles );
			}

			return self::get_ld_none_string();
		}

		/**
		 * Get LearnDash transaction price amount with currency.
		 *
		 * @param int $transaction_id - Transaction post ID.
		 *
		 * @return string - Price amount with currency prefix (e.g., 'USD 99.00') or empty string.
		 *
		 * @since 5.6.0
		 */
		public static function get_ld_price_amount_with_currency( $transaction_id ) {
			$pricing_info_meta = \get_post_meta( $transaction_id, 'pricing_info', true );
			$pricing_info      = \maybe_unserialize( $pricing_info_meta );

			$price_amount = '';
			$currency     = '';

			if ( \is_array( $pricing_info ) ) {
				$price_amount = $pricing_info['price'] ?? '';
				$currency     = $pricing_info['currency'] ?? '';
			}

			if ( ! empty( $currency ) && ! empty( $price_amount ) ) {
				$price_amount = $currency . ' ' . $price_amount;
			}

			return $price_amount;
		}

		/**
		 * Get the admin edit link for a LearnDash transaction post.
		 *
		 * @param \WP_Post $transaction_post - Transaction post object.
		 *
		 * @return string - Admin URL to edit the transaction or its parent order.
		 *
		 * @since 5.6.0
		 */
		public static function get_ld_tx_post_edit_link( $transaction_post ) {
			$transaction_id        = (int) $transaction_post->ID;
			$parent_transaction_id = (int) $transaction_post->post_parent;
			$order_edit_link       = '';

			if ( $parent_transaction_id > 0 ) {
				$order_edit_link = \admin_url( 'post.php?post=' . $parent_transaction_id . '&action=edit' );
			} else {
				$order_edit_link = \admin_url( 'post.php?post=' . $transaction_id . '&action=edit' );
			}

			return $order_edit_link;
		}


		/**
		 * Format progress as a percentage string.
		 *
		 * @param int $completed - Number of completed items.
		 * @param int $total - Total number of items.
		 *
		 * @return string Progress percentage formatted as "X.X%".
		 *
		 * @since 5.6.0
		 */
		private static function format_progress_percentage( $completed, $total ): string {
			if ( $total > 0 ) {
				$progress = ( $completed / $total ) * 100;
				return \round( $progress, 1 ) . '%';
			}

			return '0%';
		}

		/**
		 * Calculate course progress percentage.
		 *
		 * @param int $user_id - User ID.
		 * @param int $course_id - Course ID.
		 *
		 * @return string Progress percentage.
		 *
		 * @since 5.6.0
		 */
		public static function calculate_course_progress( $user_id, $course_id ): string {
			if ( \function_exists( 'learndash_course_get_completed_steps' ) ) {

				if ( 'sfwd-courses' === \get_post_type( $course_id ) ) {
					$completed_steps = \learndash_course_get_completed_steps( $user_id, $course_id );
					$total_steps     = \learndash_get_course_steps_count( $course_id );

					return self::format_progress_percentage( $completed_steps, $total_steps );
				}
			}

			return '0%';
		}

		/**
		 * Calculate lesson progress percentage.
		 *
		 * @param int $user_id - User ID.
		 * @param int $lesson_id - Lesson ID.
		 *
		 * @return string Progress percentage.
		 *
		 * @since 5.6.0
		 */
		public static function calculate_lesson_progress( $user_id, $lesson_id ) {
			if ( \function_exists( 'learndash_get_topic_list' ) && \function_exists( 'learndash_is_topic_complete' ) ) {
				// Get all topics for this lesson.
				$topics = \learndash_get_topic_list( $lesson_id );

				if ( ! empty( $topics ) && \is_array( $topics ) ) {
					$total_topics     = \count( $topics );
					$completed_topics = 0;

					// Count how many topics are completed.
					foreach ( $topics as $topic ) {
						if ( \learndash_is_topic_complete( $user_id, $topic->ID ) ) {
							++$completed_topics;
						}
					}

					return self::format_progress_percentage( $completed_topics, $total_topics );
				}
			}

			return '0%';
		}

		/**
		 * Get quiz result (percentage) from the most recent quiz attempt.
		 *
		 * @param int $user_id - User ID.
		 * @param int $quiz_id - Quiz ID.
		 *
		 * @return string - Quiz result as percentage string (e.g., "85%") or empty string if not available.
		 *
		 * @since 5.6.0
		 */
		public static function get_quiz_result( $user_id, $quiz_id ) {
			$quiz_attempts = \get_user_meta( $user_id, '_sfwd-quizzes', true );

			if ( empty( $quiz_attempts ) || ! \is_array( $quiz_attempts ) ) {
				return '';
			}

			// Find the most recent attempt for this quiz.
			$latest_attempt = null;
			$latest_time    = 0;

			foreach ( $quiz_attempts as $attempt ) {
				if ( isset( $attempt['quiz'] ) && (int) $attempt['quiz'] === (int) $quiz_id ) {
					$attempt_time = isset( $attempt['time'] ) ? (int) $attempt['time'] : 0;

					if ( $attempt_time > $latest_time ) {
						$latest_time    = $attempt_time;
						$latest_attempt = $attempt;
					}
				}
			}

			if ( null === $latest_attempt || ! isset( $latest_attempt['percentage'] ) ) {
				return '';
			}

			$result = round( $latest_attempt['percentage'], 2 ) . '%';

			return $result;
		}

		/**
		 * Get quiz type (release schedule) label.
		 *
		 * @param int $quiz_id - Quiz ID.
		 *
		 * @return string - Quiz type label: "Immediately", "Enrollment-based", "Specific date", or empty string.
		 *
		 * @since 5.6.0
		 */
		public static function get_quiz_type( $quiz_id ) {
			$lesson_schedule = \learndash_get_setting( $quiz_id, 'lesson_schedule' );

			$value = '';

			if ( empty( $lesson_schedule ) ) {
				$value = \esc_html__( 'Immediately', 'wp-security-audit-log' );
			}

			if ( 'visible_after' === $lesson_schedule ) {
				$value = \esc_html__( 'Enrollment-based', 'wp-security-audit-log' );
			}

			if ( 'visible_after_specific_date' === $lesson_schedule ) {
				$value = \esc_html__( 'Specific date', 'wp-security-audit-log' );
			}

			return $value;
		}

		/**
		 * Get user's course groups as comma-separated string.
		 *
		 * @param int $user_id - User ID.
		 * @param int $course_id - Course ID.
		 *
		 * @return string Comma-separated group names or 'None'.
		 *
		 * @since 5.6.0
		 */
		public static function get_user_course_groups( $user_id, $course_id ) {
			if ( ! $course_id ) {
				return self::get_ld_none_string();
			}

			if ( ! \function_exists( 'learndash_get_users_group_ids' ) || ! \function_exists( 'learndash_group_enrolled_courses' ) ) {
				return self::get_ld_none_string();
			}

			// Get all groups the user is in.
			$user_groups = \learndash_get_users_group_ids( $user_id );

			if ( empty( $user_groups ) ) {
				return self::get_ld_none_string();
			}

			// Filter groups that have this course.
			$course_groups = array();

			foreach ( $user_groups as $group_id ) {
				$group_courses = \learndash_group_enrolled_courses( $group_id );
				if ( \in_array( $course_id, $group_courses, true ) ) {
					$course_groups[] = \get_the_title( $group_id );
				}
			}

			if ( empty( $course_groups ) ) {
				return self::get_ld_none_string();
			}

			return \implode( ', ', $course_groups );
		}

		/**
		 * Build event data arrays for all quiz answer submissions.
		 *
		 * @param object $quiz_attempt - Quiz attempt record from wp_learndash_pro_quiz_statistic_ref table.
		 * @param array  $answers - Array of answer records from wp_learndash_pro_quiz_statistic table.
		 *
		 * @return array Array of event data arrays, one for each answer.
		 *
		 * @since 5.6.0
		 */
		public static function get_quiz_answers_event_data( $quiz_attempt, $answers ): array {
			if ( empty( $answers ) || ! $quiz_attempt ) {
				return array();
			}

			$user_id      = (int) $quiz_attempt->user_id;
			$quiz_post_id = (int) $quiz_attempt->quiz_post_id;
			$course_id    = (int) $quiz_attempt->course_post_id;

			$user = \get_user_by( 'ID', $user_id );

			if ( ! $user ) {
				return array();
			}

			$first_name = \get_user_meta( $user_id, 'first_name', true );
			$last_name  = \get_user_meta( $user_id, 'last_name', true );

			$quiz_title    = \get_the_title( $quiz_post_id );
			$course_title  = $course_id ? \get_the_title( $course_id ) : '';
			$course_groups = self::get_user_course_groups( $user_id, $course_id );

			$price_type = '';

			if ( $course_id ) {
				$course_meta = \get_post_meta( $course_id, '_sfwd-courses', true );
				$price_type  = $course_meta['sfwd-courses_course_price_type'] ?? '';
			}

			// Calculate quiz totals.
			$total_questions = \count( $answers );
			$correct_count   = 0;
			$total_points    = 0;

			foreach ( $answers as $answer ) {
				$correct_count += (int) $answer->correct_count;
				$total_points  += (float) $answer->points;
			}

			// Build event data for each answer.
			$events_data = array();

			foreach ( $answers as $index => $answer ) {
				$question_number = $index + 1;
				$question_title  = \get_the_title( $answer->question_post_id );
				$is_correct      = (int) $answer->correct_count === 1;
				$points_earned   = (float) $answer->points;
				$quiz_progress   = self::format_progress_percentage( $question_number, $total_questions );

				$events_data[] = array(
					'TargetUsername' => $user->user_login,
					'FirstName'      => $first_name,
					'LastName'       => $last_name,
					'QuizTitle'      => $quiz_title,
					'QuizProgress'   => $quiz_progress,
					'CourseGroup'    => $course_groups,
					'PriceType'      => $price_type,
					'CourseTitle'    => $course_title,
					'QuestionTitle'  => $question_title,
					'IsCorrect'      => $is_correct ? 'Yes' : 'No',
					'PointsEarned'   => $points_earned,
					'TotalQuestions' => $total_questions,
					'CorrectAnswers' => $correct_count,
					'TotalScore'     => $total_points,
				);
			}

			return $events_data;
		}

		/**
		 * Builds the event variables array for LearnDash view/open events.
		 *
		 * @param \WP_Post $post - Post object.
		 *
		 * @return array
		 *
		 * @since 5.6.2
		 */
		public static function build_ld_view_event_variables( $post ): array {
			$author_name = self::get_author_display_name( $post );

			$variables = array(
				'PostTitle'  => \esc_html( $post->post_title ),
				'PostID'     => $post->ID,
				'PostStatus' => $post->post_status,
				'Category'   => self::get_post_categories( $post->ID ),
			);

			if ( 'sfwd-courses' === $post->post_type ) {
				$variables['CourseAuthor']   = $author_name;
				$variables['CourseCategory'] = self::get_ld_post_categories( $post->ID, $post->post_type );
			} elseif ( 'sfwd-lessons' === $post->post_type ) {
				$variables['LessonAuthor']   = $author_name;
				$variables['LessonCategory'] = self::get_ld_post_categories( $post->ID, $post->post_type );
			}

			return $variables;
		}
	}
}
