<?php

namespace Limb_Chatbot\Includes\Services\Chatbot_Analytics\Calculators;

use Limb_Chatbot\Includes\Data_Objects\Chat;
use Limb_Chatbot\Includes\Data_Objects\Chat_Meta;
use Limb_Chatbot\Includes\Data_Objects\Chat_Participant;
use Limb_Chatbot\Includes\Data_Objects\Chatbot;
use Limb_Chatbot\Includes\Services\Chatbot_Analytics\Data\Analytics_Data;
use Limb_Chatbot\Includes\Services\Chatbot_Analytics\Data\Analytics_Data_Point;
use Limb_Chatbot\Includes\Services\Chatbot_Analytics\Types\Analytics_Type;

/**
 * Class Returning_Visitors_Calculator
 *
 * Calculates returning visitors analytics by counting unique chatbot user records
 * who have participated in chats both in the past AND in the given period.
 * Supports flexible period bucketing (hourly, daily, weekly, monthly, yearly).
 *
 * @package Limb_Chatbot\Includes\Services\Chatbot_Analytics\Calculators
 * @since 1.0.0
 */
class Returning_Visitors_Calculator implements Analytics_Calculator {

	/**
	 * Calculate returning visitors analytics for a chatbot.
	 *
	 * @param Analytics_Type $type          The analytics type (must be 'returning_visitors')
	 * @param int|null       $starting_from Unix timestamp in UTC+0 or null for 'all'
	 * @param int            $ending_at    Unix timestamp in UTC+0 for the end of the period
	 * @param Chatbot        $chatbot       The chatbot to filter by
	 * @param float          $utc_offset    UTC offset in hours for timezone conversion (default: 0.0)
	 *
	 * @return Analytics_Data The calculated returning visitors analytics
	 * @since 1.0.0
	 */
	public function calculate( Analytics_Type $type, ?int $starting_from, int $ending_at, Chatbot $chatbot, float $utc_offset = 0.0 ): Analytics_Data {

		// Determine bucketing strategy based on time range
		$strategy = Period_Bucketing::get_strategy( $starting_from, $ending_at );

		// Get buckets for this strategy (with UTC offset for timezone conversion)
		$buckets = Period_Bucketing::get_buckets( $strategy, $starting_from, $ending_at, $utc_offset );

		// Fetch returning visitor participation data from database
		// Returns Chat_Participant objects for bucketing by joined_at
		$all_participants = $this->get_returning_visitor_records( $chatbot, $starting_from, $ending_at );

		// Create data points by aggregating participants into buckets
		$data_points = [];
		foreach ( $buckets as $bucket ) {
			$count = $this->count_visitors_in_bucket( $all_participants, $bucket['start'], $bucket['end'] );

			$data_points[] = new Analytics_Data_Point(
				$bucket['label'],
				$count
			);
		}

		// Current total is count of unique users in the period
		// Since we already filter to one record per user per period, just count the array
		$current_total = count( $all_participants );

		// Calculate comparison with previous period
		$comparison_result = $this->calculate_comparison(
			$starting_from,
			$ending_at,
			$current_total,
			$chatbot
		);

		// Determine period label based on starting_from
		$period_label = $this->determine_period_label( $starting_from, $ending_at );

		// Create analytics data object
		$analytics_data = new Analytics_Data(
			$type,
			$period_label,
			$starting_from,
			$data_points
		);

		// Set the period totals
		$analytics_data->set_current_period_total( $comparison_result['current_total'] );
		$analytics_data->set_previous_period_total( $comparison_result['previous_total'] );

		return $analytics_data;
	}

	/**
	 * Get user IDs who have participated in more than one chat.
	 *
	 * Caches the result to avoid multiple queries.
	 *
	 * @return array Array of user IDs with multiple chat participations
	 * @since 1.0.0
	 */
	private function get_returning_user_ids(): array {
		$preview_chat_ids = $this->get_preview_chat_ids_for_chatbot();
		// Get ALL Chat_Participant records
		$where = [];
		if ( ! empty( $preview_chat_ids ) ) {
			$where['chat_id NOT IN'] = $preview_chat_ids;
		}
		$all_participants = Chat_Participant::where( $where )->get();

		// Group by user_id and collect unique chat_ids
		$user_chats = [];
		foreach ( $all_participants as $participant ) {
			if ( ! isset( $user_chats[ $participant->user_id ] ) ) {
				$user_chats[ $participant->user_id ] = [];
			}
			if ( ! in_array( $participant->chat_id, $user_chats[ $participant->user_id ], true ) ) {
				$user_chats[ $participant->user_id ][] = $participant->chat_id;
			}
		}

		// Filter to users with more than one chat (returning visitors)
		$returning_user_ids = [];
		foreach ( $user_chats as $user_id => $chat_ids ) {
			if ( count( $chat_ids ) > 1 ) {
				$returning_user_ids[] = $user_id;
			}
		}

		return $returning_user_ids;
	}

	/**
	 * Fetch all returning visitor records for a chatbot within optional time range.
	 *
	 * Returning visitors are users who:
	 * 1. Have participated in MORE THAN ONE chat (multiple chat_ids)
	 * 2. AND have at least one participation in the given period
	 *
	 * Gets Chat_Participant records, filters to users with multiple chats,
	 * then gets their participation records from the given period.
	 *
	 * @param Chatbot  $chatbot        The chatbot
	 * @param int|null $starting_from  Unix timestamp or null for 'all'
	 * @param int      $ending_at      Unix timestamp
	 *
	 * @return array Array of Chat_Participant objects representing returning visitors in the period
	 * @since 1.0.0
	 */
	private function get_returning_visitor_records( Chatbot $chatbot, ?int $starting_from, int $ending_at ): array {
		// Get users with multiple chat participations
		$returning_user_ids = $this->get_returning_user_ids();
		$preview_chat_ids   = $this->get_preview_chat_ids_for_chatbot();

		if ( empty( $returning_user_ids ) ) {
			return [];
		}

		// Get participants in the period for these returning users
		$period_where = [
			'user_id' => $returning_user_ids,
		];
		if ( ! empty( $preview_chat_ids ) ) {
			$period_where['chat_id NOT IN'] = $preview_chat_ids;
		}

		if ( null !== $starting_from ) {
			$start_datetime = gmdate( 'Y-m-d H:i:s', $starting_from );
			$end_datetime   = gmdate( 'Y-m-d H:i:s', $ending_at );

			$period_where['joined_at>='] = $start_datetime;
			$period_where['joined_at<='] = $end_datetime;
		}

		// Get participants in period
		$current_period_participants = Chat_Participant::where( $period_where )->get();

		// Filter to first participation per user in the period (unique users only)
		$unique_user_ids = [];
		$filtered_participants = [];
		foreach ( $current_period_participants as $participant ) {
			if ( ! isset( $unique_user_ids[ $participant->user_id ] ) ) {
				$unique_user_ids[ $participant->user_id ] = true;
				$filtered_participants[] = $participant;
			}
		}

		return $filtered_participants;
	}

	/**
	 * Count unique visitors (by user_id) within a specific bucket (time range).
	 *
	 * @param array $participants Array of Chat_Participant objects
	 * @param int   $bucket_start Unix timestamp for bucket start
	 * @param int   $bucket_end   Unix timestamp for bucket end
	 *
	 * @return int Count of unique visitors in this bucket
	 * @since 1.0.0
	 */
	private function count_visitors_in_bucket( array $participants, int $bucket_start, int $bucket_end ): int {
		$unique_users = [];

		foreach ( $participants as $participant ) {
			// Convert joined_at string to Unix timestamp
			// Format: '2026-01-08 09:27:35'
			$joined_timestamp = strtotime( $participant->joined_at );

			// Check if this participant joined within the bucket time range
			if ( $joined_timestamp >= $bucket_start && $joined_timestamp < $bucket_end ) {
				// Only count each user once per bucket
				$unique_users[ $participant->user_id ] = true;
			}
		}

		return count( $unique_users );
	}

	/**
	 * Calculate comparison with previous period.
	 *
	 * Compares current period returning visitor count with previous same-length period.
	 * Follows the same logic: users with multiple chats who participated in the previous period.
	 * Returns an array with both period totals.
	 *
	 * @param int|null $starting_from Unix timestamp or null for 'all'
	 * @param int      $ending_at     Unix timestamp
	 * @param int      $current_count Current period unique user count
	 * @param Chatbot  $chatbot       The chatbot
	 *
	 * @return array Array with keys: 'current_total', 'previous_total'
	 * @since 1.0.0
	 */
	private function calculate_comparison( ?int $starting_from, int $ending_at, $current_count, Chatbot $chatbot ): array {
		if ( null === $starting_from ) {
			// For 'all', no previous period to compare
			return [
				'current_total'  => $current_count,
				'previous_total' => 0,
			];
		}

		$period_length = $ending_at - $starting_from;
		$prev_start    = $starting_from - $period_length;
		$prev_end      = $starting_from;

		// Get database formatted dates
		$prev_start_date = gmdate( 'Y-m-d H:i:s', $prev_start );
		$prev_end_date   = gmdate( 'Y-m-d H:i:s', $prev_end );

		// Get returning user IDs (users with 2+ chats globally)
		$returning_user_ids = $this->get_returning_user_ids();

		// If no returning users, return 0
		if ( empty( $returning_user_ids ) ) {
			return [
				'current_total'  => $current_count,
				'previous_total' => 0,
			];
		}

		// Get participants in previous period for returning users
		$prev_period_participants = Chat_Participant::where( [
			'user_id'         => $returning_user_ids,
			'joined_at>='     => $prev_start_date,
			'joined_at<='     => $prev_end_date,
			'chat_id NOT IN'  => $this->get_preview_chat_ids_for_chatbot(),
		] )->get();

		// Count unique users in previous period
		$previous_user_ids = [];
		foreach ( $prev_period_participants as $participant ) {
			$previous_user_ids[ $participant->user_id ] = true;
		}

		$previous_count = count( $previous_user_ids );

		return [
			'current_total'  => $current_count,
			'previous_total' => $previous_count,
		];
	}

	/**
	 * Get preview chat IDs, optionally scoped to a chatbot.
	 *
	 * @return int[]
	 */
	private function get_preview_chat_ids_for_chatbot(): array {
		$preview_metas = Chat_Meta::where( [ 'meta_key' => 'preview' ], -1, -1 );
		if ( $preview_metas->is_empty() ) {
			return [];
		}

		return array_values(
			array_unique(
				array_filter( array_map( 'absint', $preview_metas->pluck( 'chat_id' ) ) )
			)
		);
	}

	/**
	 * Determine human-readable period label.
	 *
	 * @param int|null $starting_from Unix timestamp or null for 'all'
	 * @param int      $ending_at     Unix timestamp
	 *
	 * @return string Period label
	 * @since 1.0.0
	 */
	private function determine_period_label( ?int $starting_from, int $ending_at ): string {
		if ( null === $starting_from ) {
			return __( 'All Time', 'limb-chatbot' );
		}

		$diff = $ending_at - $starting_from;
		$days = (int) ( $diff / ( 24 * 60 * 60 ) );

		if ( $days < 1 ) {
			$hours = (int) ( $diff / ( 60 * 60 ) );
			if ( $hours < 1 ) {
				$minutes = (int) ( $diff / 60 );
				return sprintf( _n( 'Last %d Minute', 'Last %d Minutes', $minutes, 'limb-chatbot' ), $minutes );
			}
			return sprintf( _n( 'Last %d Hour', 'Last %d Hours', $hours, 'limb-chatbot' ), $hours );
		}

		return sprintf( _n( 'Last %d Day', 'Last %d Days', $days, 'limb-chatbot' ), $days );
	}
}
