<?php

namespace Limb_Chatbot\Includes\Services\Chatbot_Analytics;

use Limb_Chatbot\Includes\Data_Objects\Chatbot;
use Limb_Chatbot\Includes\Factories\Chatbot_Analytics_Factory;
use Limb_Chatbot\Includes\Services\Chatbot_Analytics\Calculators\Calculator_Registry;
use Limb_Chatbot\Includes\Services\Chatbot_Analytics\Data\Analytics_Data;
use Limb_Chatbot\Includes\Services\Chatbot_Analytics\Types\Analytics_Type_Registry;

/**
 * Class Chatbot_Analytics_Service
 *
 * Service layer for retrieving and processing chatbot analytics.
 * Follows SOLID principles:
 * - Single Responsibility: Only handles analytics data retrieval and aggregation
 * - Dependency Injection: Receives factory to setup types
 * - Open/Closed: Easy to extend with new analytics calculations
 *
 * @package Limb_Chatbot\Includes\Services\Chatbot_Analytics
 * @since 1.0.0
 */
class Chatbot_Analytics_Service {

	/**
	 * The analytics type registry.
	 *
	 * @var Analytics_Type_Registry
	 * @since 1.0.0
	 */
	private Analytics_Type_Registry $registry;

	/**
	 * The calculator registry.
	 *
	 * @var Calculator_Registry
	 * @since 1.0.0
	 */
	private Calculator_Registry $calculator_registry;

	/**
	 * Chatbot_Analytics_Service constructor.
	 *
	 * @since 1.0.0
	 */
	public function __construct() {
		$this->registry              = Analytics_Type_Registry::instance();
		$this->calculator_registry   = new Calculator_Registry();
	}

	/**
	 * Get analytics data for a chatbot.
	 *
	 * @param Chatbot              $chatbot       The chatbot instance
	 * @param array                $include       Array of type IDs to include (empty = all)
	 * @param int|string|null      $starting_from Unix timestamp or datetime string (e.g., '2025-01-01 00:00:00')
	 *                                            in UTC+0 format, or 'all' to include all data without time filtering
	 * @param int|string|null      $ending_at     Unix timestamp in seconds (UTC+0), or null to use the current time.
	 * @param float                $utc_offset    UTC offset in hours (e.g., 4.0 for UTC+4, -4.0 for UTC-4)
	 *
	 * @return Analytics_Data[] Array of analytics data objects
	 * @since 1.0.0
	 */
	public function get_analytics( Chatbot $chatbot, array $include = [], $starting_from = null, float $utc_offset = 0.0, $ending_at = null ): array {
		// Initialize analytics factory to register action types and calculators
		$factory = new Chatbot_Analytics_Factory( $chatbot, $this->calculator_registry );

		// Get registry (now with both built-in and action types registered)
		$registry = $factory->get_registry();

		// Determine which types to include
		$types_to_process = $this->get_types_to_process( $registry, $include );

		// Convert starting_from from user's timezone (UTC+X) to UTC+0 for database queries
		// Frontend sends starting_from in UTC+0, but it represents the user's local time
		// So we need to convert: user_local_time - utc_offset = UTC+0 time
		$timestamp = $this->parse_starting_from( $starting_from, $utc_offset );
		$ending_at_utc0 = $this->parse_ending_at( $ending_at );

		$analytics_collection = [];

		foreach ( $types_to_process as $type ) {
			// Check if a calculator is available for this type
			$calculator = $this->calculator_registry->get( $type );

			if ( null !== $calculator ) {
				// Use the calculator to compute analytics (pass UTC offset for timezone conversion)
				$analytics_data = $calculator->calculate( $type, $timestamp, $ending_at_utc0, $chatbot, $utc_offset );
			} else {
				// Fallback: create empty analytics data structure
				$period = $this->determine_period_label( $starting_from, $ending_at_utc0 );
				$analytics_data = new Analytics_Data(
					$type,
					$period,
					$timestamp,
					[]
				);
			}

			$analytics_collection[] = $analytics_data;
		}

		return $analytics_collection;
	}

	/**
	 * Parse the starting_from parameter into a unix timestamp in UTC+0.
	 *
	 * The frontend sends starting_from in UTC+0 format, but it represents the user's local time.
	 * We convert it to actual UTC+0 time by subtracting the UTC offset.
	 *
	 * Supports:
	 * - Unix timestamp (int) - treated as UTC+0
	 * - DateTime string (e.g., '2025-01-01 00:00:00') - treated as UTC+0 but represents user's local time
	 * - 'all' (no time filtering)
	 * - null (current time)
	 *
	 * @param int|string|null $starting_from The starting point value (in UTC+0 format, but represents user's local time)
	 * @param float            $utc_offset    UTC offset in hours (e.g., 4.0 for UTC+4)
	 *
	 * @return int|null Unix timestamp in UTC+0, or null if 'all' or if unable to parse
	 * @since 1.0.0
	 */
	private function parse_starting_from( $starting_from, float $utc_offset = 0.0 ): ?int {
		// If 'all', return null (indicates no time filtering)
		if ( 'all' === $starting_from ) {
			return null;
		}

		$timestamp = null;

		// If already a unix timestamp (int)
		if ( is_int( $starting_from ) ) {
			$timestamp = $starting_from;
		} elseif ( is_string( $starting_from ) && ! empty( $starting_from ) ) {
			// If it's a datetime string, parse it (assumed to be in UTC+0 format)
			$timestamp = strtotime( $starting_from . ' UTC' );
			if ( false === $timestamp ) {
				return null;
			}
		} else {
			// If null or invalid, return null
			return null;
		}

		// The frontend sends starting_from in UTC+0 format, but it represents the user's local time.
		// For example, if user is in UTC+4 and frontend sends "2025-01-31 20:00:00",
		// it means "2025-02-01 00:00:00 UTC+4", which is already "2025-01-31 20:00:00 UTC+0".
		// So the timestamp is already correct for UTC+0 database queries - no conversion needed here.
		// The UTC offset is only used for bucket label generation and response timestamp conversion.

		return $timestamp;
	}

	/**
	 * Parse the ending_at parameter into a Unix timestamp in UTC+0.
	 *
	 * Frontend should send `end_to` as Unix seconds (e.g., 1773604799).
	 * If it's missing/empty/null, default to the current time.
	 *
	 * @param int|string|null $ending_at Unix timestamp (seconds) in UTC+0
	 *
	 * @return int Unix timestamp in UTC+0
	 * @since 1.0.0
	 */
	private function parse_ending_at( $ending_at ): int {
		// Missing/empty/null => use current time.
		if ( null === $ending_at || '' === $ending_at ) {
			return time();
		}

		// If already an int, use it (but guard against 0/negative values).
		if ( is_int( $ending_at ) ) {
			return $ending_at > 0 ? $ending_at : time();
		}

		// If numeric string, treat it as unix seconds.
		if ( is_string( $ending_at ) ) {
			$trimmed = trim( $ending_at );
			if ( $trimmed === '' ) {
				return time();
			}

			if ( ctype_digit( $trimmed ) ) {
				$ts = (int) $trimmed;
				return $ts > 0 ? $ts : time();
			}

			// Last resort: try to parse as datetime string in UTC.
			$ts = strtotime( $trimmed . ' UTC' );
			if ( false !== $ts && $ts > 0 ) {
				return (int) $ts;
			}
		}

		return time();
	}

	/**
	 * Determine a period label based on the starting_from parameter.
	 *
	 * @param int|string|null $starting_from The starting point value
	 * @param int             $ending_at     Unix timestamp (UTC+0)
	 *
	 * @return string Period label for display
	 * @since 1.0.0
	 */
	private function determine_period_label( $starting_from, int $ending_at ): string {
		// If 'all', return all-time label
		if ( 'all' === $starting_from ) {
			return __( 'All Time', 'limb-chatbot' );
		}

		// If it's a datetime string, try to parse and calculate period
		if ( is_string( $starting_from ) && ! empty( $starting_from ) ) {
			$timestamp = strtotime( $starting_from );
			if ( false !== $timestamp ) {
				return $this->calculate_period_label( $timestamp, $ending_at );
			}
		}

		// If it's a unix timestamp
		if ( is_int( $starting_from ) ) {
			return $this->calculate_period_label( $starting_from, $ending_at );
		}

		// Default
		return __( 'Last 7 Days', 'limb-chatbot' );
	}

	/**
	 * Calculate a period label based on a unix timestamp.
	 *
	 * @param int $timestamp Unix timestamp
	 * @param int $ending_at Unix timestamp for the end of the period
	 *
	 * @return string Period label
	 * @since 1.0.0
	 */
	private function calculate_period_label( int $timestamp, int $ending_at ): string {
		$diff    = $ending_at - $timestamp;
		$days    = (int) ( $diff / ( 24 * 60 * 60 ) );
		$hours   = (int) ( $diff / ( 60 * 60 ) );
		$minutes = (int) ( $diff / 60 );

		if ( $days > 0 ) {
			return sprintf( _n( 'Last %d Day', 'Last %d Days', $days, 'limb-chatbot' ), $days );
		} elseif ( $hours > 0 ) {
			return sprintf( _n( 'Last %d Hour', 'Last %d Hours', $hours, 'limb-chatbot' ), $hours );
		} elseif ( $minutes > 0 ) {
			return sprintf( _n( 'Last %d Minute', 'Last %d Minutes', $minutes, 'limb-chatbot' ), $minutes );
		}

		return __( 'Current', 'limb-chatbot' );
	}

	/**
	 * Determine which analytics types to process based on include filter.
	 *
	 * @param Analytics_Type_Registry $registry The type registry
	 * @param array                   $include  Array of type IDs to include (empty = all)
	 *
	 * @return array Array of Analytics_Type objects to process
	 * @since 1.0.0
	 */
	private function get_types_to_process( Analytics_Type_Registry $registry, array $include ): array {
		if ( empty( $include ) ) {
			// Return all registered types
			return $registry->get_all();
		}

		// Return only the requested types
		$types = [];
		foreach ( $include as $type_id ) {
			$type = $registry->get( $type_id );
			if ( null !== $type ) {
				$types[] = $type;
			}
		}

		return $types;
	}

	/**
	 * Get the analytics type registry for direct access if needed.
	 *
	 * @return Analytics_Type_Registry
	 * @since 1.0.0
	 */
	public function get_registry(): Analytics_Type_Registry {
		return $this->registry;
	}
}