<?php
/**
 * Activity Tracker Utility
 *
 * Handles user activity tracking for spaces and unread post counts.
 *
 * @package SureDash
 * @since 1.6.0
 */

namespace SureDashboard\Inc\Utils;

use SureDashboard\Inc\Traits\Get_Instance;

defined( 'ABSPATH' ) || exit;

/**
 * Class Activity_Tracker
 *
 * @since 1.6.0
 */
class Activity_Tracker {
	use Get_Instance;

	/**
	 * Meta key prefix for last visit timestamps.
	 */
	private const META_KEY_PREFIX = 'suredash_space_last_visit_';

	/**
	 * Cache group for transients.
	 */
	private const CACHE_GROUP = 'suredash_unread_counts';

	/**
	 * Cache expiration time in seconds (2 minutes).
	 */
	private const CACHE_EXPIRATION = 120;

	/**
	 * Update the last visit timestamp for a user in a specific space.
	 *
	 * @param int $user_id User ID.
	 * @param int $space_id Space ID (term_id from portal_group taxonomy).
	 * @return bool True on success, false on failure.
	 * @since 1.6.0
	 */
	public function update_space_last_visit( $user_id, $space_id ) {
		if ( empty( $user_id ) || empty( $space_id ) ) {
			return false;
		}

		$meta_key = self::META_KEY_PREFIX . $space_id;
		$result   = sd_update_user_meta( $user_id, $meta_key, time() );

		// Clear cached unread count for this space.
		$this->clear_cache( $user_id, $space_id );

		return $result !== false;
	}

	/**
	 * Get unread posts count for a user in a specific space.
	 * Uses counter-based calculation for scalability: total posts - viewed count = unread count.
	 * Only counts posts the user has permission to view (visibility_scope + SureMembers).
	 *
	 * @param int $user_id User ID.
	 * @param int $space_id Space ID (term_id from community-forum taxonomy).
	 * @return int Number of unread posts.
	 * @since 1.6.0
	 */
	public function get_unread_posts_count( $user_id, $space_id ) {
		if ( empty( $user_id ) || empty( $space_id ) ) {
			return 0;
		}

		// Try to get from cache first.
		$cache_key = $this->get_cache_key( $user_id, $space_id );
		$cached    = get_transient( $cache_key );

		if ( $cached !== false ) {
			return absint( $cached );
		}

		// Get visible posts count for this user in the space.
		$visible_total = $this->get_visible_posts_count_in_space( $space_id, $user_id );

		if ( $visible_total === 0 ) {
			set_transient( $cache_key, 0, self::CACHE_EXPIRATION );
			return 0;
		}

		// Get count of posts this user has already viewed.
		$viewed_count = $this->get_viewed_count( $user_id, $space_id );

		// Self-heal: if viewed_count exceeds visible total (e.g. stale counter from
		// before visibility filtering was added, or posts were deleted/restricted),
		// reset it so new posts are properly counted as unread going forward.
		if ( $viewed_count > $visible_total ) {
			$meta_key = 'suredash_viewed_count_' . $space_id;
			sd_update_user_meta( $user_id, $meta_key, $visible_total );
			$viewed_count = $visible_total;
		}

		$unread_count = max( 0, $visible_total - $viewed_count );

		// Cache the result.
		set_transient( $cache_key, $unread_count, self::CACHE_EXPIRATION );

		return $unread_count;
	}

	/**
	 * Mark posts as viewed when user visits a space.
	 * Uses a counter-based approach for scalability with large post counts.
	 * Counts only posts the user can actually see.
	 *
	 * @param int $user_id User ID.
	 * @param int $space_id Space ID.
	 * @param int $post_id Optional specific post ID to mark as viewed.
	 * @return bool True on success.
	 * @since 1.6.0
	 */
	public function mark_posts_as_viewed( $user_id, $space_id, $post_id = 0 ) {
		if ( empty( $user_id ) || empty( $space_id ) ) {
			return false;
		}

		$meta_key     = 'suredash_viewed_count_' . $space_id;
		$viewed_count = absint( sd_get_user_meta( $user_id, $meta_key, true ) );

		if ( ! empty( $post_id ) ) {
			// Increment counter by 1 for single post view.
			$viewed_count++;
		} else {
			// Mark ALL visible posts in space as viewed.
			$viewed_count = $this->get_visible_posts_count_in_space( $space_id, $user_id );
		}

		// Update user meta with new count.
		$result = sd_update_user_meta( $user_id, $meta_key, $viewed_count );

		// Clear cache.
		$this->clear_cache( $user_id, $space_id );

		return $result !== false;
	}

	/**
	 * Get unread counts for all spaces for a specific user.
	 *
	 * @param int $user_id User ID.
	 * @return array<int,int> Array of space_id => unread_count pairs.
	 * @since 1.6.0
	 */
	public function get_all_unread_counts( $user_id ) {
		if ( empty( $user_id ) ) {
			return [];
		}

		// Get all forum terms (spaces).
		$terms = get_terms(
			[
				'taxonomy'   => SUREDASHBOARD_FEED_TAXONOMY,
				'hide_empty' => false,
			]
		);

		if ( is_wp_error( $terms ) || empty( $terms ) ) {
			return [];
		}

		$unread_counts = [];

		foreach ( $terms as $term ) {
			$count = $this->get_unread_posts_count( $user_id, $term->term_id );
			if ( $count > 0 ) {
				$unread_counts[ $term->term_id ] = $count;
			}
		}

		return $unread_counts;
	}

	/**
	 * Mark a space as read for a user (set last visit to now).
	 *
	 * @param int $user_id User ID.
	 * @param int $space_id Space ID.
	 * @return bool True on success, false on failure.
	 * @since 1.6.0
	 */
	public function mark_space_as_read( $user_id, $space_id ) {
		return $this->update_space_last_visit( $user_id, $space_id );
	}

	/**
	 * Invalidate cache when a new post is created.
	 * Called via hook when a community post is published.
	 *
	 * @param int $post_id Post ID.
	 * @return void
	 * @since 1.6.0
	 */
	public function invalidate_cache_on_new_post( $post_id ): void {
		// Get the space/forum this post belongs to.
		$terms = wp_get_post_terms( $post_id, SUREDASHBOARD_FEED_TAXONOMY, [ 'fields' => 'ids' ] );

		if ( is_wp_error( $terms ) || empty( $terms ) ) {
			return;
		}

		// Clear cache for all users (since we don't know who has visited this space).
		// This is acceptable since transients will regenerate on next request.
		foreach ( $terms as $term_id ) {
			$this->clear_cache_for_space( $term_id );
		}
	}

	/**
	 * Get count of posts that user has viewed in a space.
	 * For first-time access (no counter exists), initialize counter to current
	 * visible total to prevent showing all existing posts as unread.
	 *
	 * @param int $user_id User ID.
	 * @param int $space_id Space ID.
	 * @return int Count of viewed posts.
	 * @since 1.6.0
	 */
	private function get_viewed_count( $user_id, $space_id ) {
		$meta_key = 'suredash_viewed_count_' . $space_id;
		$count    = sd_get_user_meta( $user_id, $meta_key, true );

		// First time - no counter exists yet.
		// Initialize to visible total so existing posts aren't shown as "unread".
		if ( $count === '' || $count === false ) {
			$visible_total = $this->get_visible_posts_count_in_space( $space_id, $user_id );
			sd_update_user_meta( $user_id, $meta_key, $visible_total );
			return $visible_total;
		}

		return absint( $count );
	}

	/**
	 * Get total count of published posts in a space (unfiltered).
	 *
	 * @param int $space_id Space ID (term_id from community-forum taxonomy).
	 * @return array<int|\WP_Post> Array of post IDs.
	 * @since 1.6.0
	 */
	private function get_post_ids_in_space( $space_id ): array {
		$args = [
			'post_type'      => SUREDASHBOARD_FEED_POST_TYPE,
			'post_status'    => 'publish',
			'tax_query'      => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
				[
					'taxonomy' => SUREDASHBOARD_FEED_TAXONOMY,
					'field'    => 'term_id',
					'terms'    => $space_id,
				],
			],
			'fields'         => 'ids',
			'posts_per_page' => -1,
			'no_found_rows'  => true,
		];

		$query = new \WP_Query( $args );

		return $query->posts;
	}

	/**
	 * Get count of published posts in a space that the given user can see.
	 *
	 * Admins (portal managers) see all posts. Regular users only see posts
	 * not protected by visibility_scope or SureMembers access groups.
	 *
	 * @param int $space_id Space ID (term_id from community-forum taxonomy).
	 * @param int $user_id  User ID.
	 * @return int Visible post count.
	 * @since 1.8.1
	 */
	private function get_visible_posts_count_in_space( $space_id, $user_id ): int {
		$post_ids = $this->get_post_ids_in_space( $space_id );

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

		// Admins see everything — skip filtering.
		if ( function_exists( 'suredash_is_user_manager' ) && suredash_is_user_manager( $user_id ) ) {
			return count( $post_ids );
		}

		$visible_count = 0;
		foreach ( $post_ids as $post_id ) {
			$post_id = (int) $post_id; // @phpstan-ignore-line — WP_Query with fields=ids returns integers.

			if ( ! Helper::is_post_visible_to_user(
				[
					'post_id' => $post_id,
					'user_id' => $user_id,
				]
			) ) {
				continue;
			}

			if ( function_exists( 'suredash_is_post_protected' ) && suredash_is_post_protected( $post_id ) ) {
				continue;
			}

			$visible_count++;
		}

		return $visible_count;
	}

	/**
	 * Clear cached unread count for a specific space.
	 *
	 * @param int $user_id User ID.
	 * @param int $space_id Space ID.
	 * @return bool True on success.
	 * @since 1.6.0
	 */
	private function clear_cache( $user_id, $space_id ) {
		$cache_key = $this->get_cache_key( $user_id, $space_id );
		$result    = delete_transient( $cache_key );

		// Also clear from object cache if available (handles Redis/Memcached).
		if ( function_exists( 'wp_cache_delete' ) ) {
			wp_cache_delete( $cache_key, 'transient' );
		}

		return $result;
	}

	/**
	 * Get cache key for a user-space pair.
	 *
	 * @param int $user_id User ID.
	 * @param int $space_id Space ID.
	 * @return string Cache key.
	 * @since 1.6.0
	 */
	private function get_cache_key( $user_id, $space_id ) {
		return self::CACHE_GROUP . '_' . $user_id . '_' . $space_id;
	}

	/**
	 * Clear cached unread counts for all users for a specific space.
	 *
	 * @param int $space_id Space ID.
	 * @return void
	 * @since 1.6.0
	 */
	private function clear_cache_for_space( $space_id ): void {
		global $wpdb;

		// Delete all transients for this space.
		// Pattern: _transient_suredash_unread_counts_{user_id}_{space_id}
		// Use % wildcard for user_id part.
		$pattern = '_transient_' . self::CACHE_GROUP . '_%_' . $space_id;

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s", $pattern ) );

		// Also delete the timeout transients.
		$timeout_pattern = '_transient_timeout_' . self::CACHE_GROUP . '_%_' . $space_id;
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s", $timeout_pattern ) );

		// Flush object cache group if available.
		if ( function_exists( 'wp_cache_flush_group' ) ) {
			wp_cache_flush_group( 'transient' );
		}
	}
}
