<?php
/**
 * @package         FireBox
 * @version         3.1.1 Pro
 * 
 * @author          FirePlugins <info@fireplugins.com>
 * @link            https://www.fireplugins.com
 * @copyright       Copyright © 2025 FirePlugins All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/

namespace FireBox\Core\Analytics\Metrics;

use FireBox\Core\Analytics\QueryBuilders\Revenue\RevenueQueryStrategyFactory;

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

class Revenue extends Metric
{
	/**
	 * Get data using strategy pattern
	 */
	public function getData()
	{
		$strategy = $this->createQueryStrategy();
		
		$sql = "SELECT
				{$strategy->getSelect()}
			FROM
				{$strategy->getFromAndJoins()}
			WHERE
				1
				{$strategy->getWherePeriod()}
				{$strategy->getWhere()}
				{$strategy->getFilters()}
			{$strategy->getGroupBy()}
			{$strategy->getHaving()}
			{$strategy->getOrderBy()}
			{$strategy->getLimitOffset()}";
		
		$results = $this->executeRevenuQuery($sql);

		// Let strategy handle its own post-processing
		if (method_exists($strategy, 'processResults'))
		{
			return $strategy->processResults($results);
		}

		// Fallback: return raw results if strategy doesn't implement processResults
		return $results;
	}

	/**
	 * Execute revenue query - always returns raw results for processing
	 */
	protected function executeRevenuQuery(string $sql)
	{
		return $this->wpdb->get_results($sql); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
	}

	/**
	 * Create query strategy for this metric
	 */
	protected function createQueryStrategy()
	{
		return RevenueQueryStrategyFactory::create($this->type, $this);
	}

	/**
	 * Indicates this class handles timezone conversion in SQL
	 */
	public function hasTimezoneSQLConversion()
	{
		return true;
	}

	/**
	 * Extract and normalize order data from a database result
	 */
	protected function extractOrderData($result)
	{
		return [
			'order_type' => trim($result->order_type, '"'),
			'order_id' => intval(trim($result->order_id, '"')),
			'order_key' => trim($result->order_type, '"') . '_' . intval(trim($result->order_id, '"'))
		];
	}

	/**
	 * Build a map of orders grouped by a specific key (e.g., period, attribution type)
	 * Returns: [group_key => [order_key => [order_data + attribution_types[] + conversion_count + impression_count]]]
	 */
	protected function buildOrderAttributionMap($results, $groupKeyCallback = null)
	{
		$map = [];
		
		if (!is_array($results))
		{
			return ['all' => []];
		}

		// Build the map using global counts from SQL window functions
		foreach ($results as $result)
		{
			$orderData = $this->extractOrderData($result);
			
			if (empty($orderData['order_id']))
			{
				continue;
			}
			
			$orderKey = $orderData['order_key'];
			$groupKey = $groupKeyCallback ? $groupKeyCallback($result) : 'all';
			
			// Initialize group and order if needed
			if (!isset($map[$groupKey]))
			{
				$map[$groupKey] = [];
			}
			
			if (!isset($map[$groupKey][$orderKey]))
			{
				$map[$groupKey][$orderKey] = $orderData;
				$map[$groupKey][$orderKey]['attribution_types'] = [];
				// Use global counts from SQL window functions (calculated across ALL campaigns)
				$map[$groupKey][$orderKey]['conversion_count'] = isset($result->global_conversion_count) ? intval($result->global_conversion_count) : 0;
				$map[$groupKey][$orderKey]['impression_count'] = isset($result->global_impression_count) ? intval($result->global_impression_count) : 0;
			}
			
			// Track attribution type for this campaign's contribution
			if (isset($result->event_source))
			{
				$map[$groupKey][$orderKey]['attribution_types'][] = $result->event_source;
			}
		}
		
		return $map;
	}

	/**
	 * Calculate attributed revenue for an order based on its attribution types
	 */
	protected function calculateAttributedRevenue($orderData)
	{
		$orderTotal = $this->getOrderTotal($orderData['order_id'], $orderData['order_type']);
		
		if ($orderTotal <= 0)
		{
			return 0.0;
		}
		
		// Apply dynamic attribution based on context
		if ($this->shouldApplyAttribution() && isset($orderData['attribution_types']))
		{
			// Use global counts from buildOrderAttributionMap
			$attribution_types = $orderData['attribution_types'];
			$conversion_count = $orderData['conversion_count'] ?? 0;
			$impression_count = $orderData['impression_count'] ?? 0;
			
			$attribution_percentage = $this->calculateAttributionPercentage(
				$attribution_types, 
				$conversion_count,
				$impression_count
			);
			return $orderTotal * $attribution_percentage;
		}
		
		return $orderTotal;
	}

	/**
	 * Calculate total revenue from database results
	 */
	public function calculateTotalRevenue($results)
	{
		$orderMap = $this->buildOrderAttributionMap($results);
		
		// Handle empty results
		if (!isset($orderMap['all']) || empty($orderMap['all']))
		{
			return 0.0;
		}
		
		$total_revenue = 0.0;
		foreach ($orderMap['all'] as $orderData)
		{
			$total_revenue += $this->calculateAttributedRevenue($orderData);
		}
		
		return $total_revenue;
	}

	/**
	 * Calculate revenue breakdown by attribution type
	 */
	public function calculateAttributionBreakdown($results)
	{
		$orderMap = $this->buildOrderAttributionMap($results, function($result) {
			return $result->event_source; // Group by attribution type
		});
		
		$breakdown_results = [];
		
		foreach (['impression', 'conversion'] as $attribution_type)
		{
			$orders = isset($orderMap[$attribution_type]) ? $orderMap[$attribution_type] : [];
			$total_revenue = 0.0;
			$order_count = 0;
			
			foreach ($orders as $orderData)
			{
				$revenue = $this->calculateAttributedRevenue($orderData);
				if ($revenue > 0)
				{
					$total_revenue += $revenue;
					$order_count++;
				}
			}
			
			$avg_order_value = $order_count > 0 ? $total_revenue / $order_count : 0.0;
			
			$breakdown_results[] = (object) [
				'attribution_type' => $attribution_type,
				'order_count' => $order_count,
				'total_revenue' => $total_revenue,
				'avg_order_value' => $avg_order_value
			];
		}
		
		return $breakdown_results;
	}

	/**
	 * Calculate attribution breakdown grouped by time period
	 */
	public function calculateAttributionBreakdownByPeriod($results)
	{
		if (empty($results))
		{
			return [];
		}

		$orderMap = $this->buildOrderAttributionMap($results, function($result)
		{
			return $result->label; // Group by period label  
		});
		
		$formatted_results = [];
		
		foreach ($orderMap as $period_key => $orders)
		{
			$revenue_by_type = [
				'impression' => 0.0,
				'conversion' => 0.0
			];
			
			foreach ($orders as $orderData)
			{
				$revenue = $this->calculateAttributedRevenue($orderData);
				
				if ($revenue > 0)
				{
					// Multi-touch: Add revenue for each attribution (don't use array_unique)
					// If 2 conversions share €100 (€50 each), we add €50 twice to conversion = €100 total
					$attribution_types = $orderData['attribution_types'];
					foreach ($attribution_types as $attribution_type)
					{
						$revenue_by_type[$attribution_type] += $revenue;
					}
				}
			}
			
			$formatted_results[] = (object) [
				'label' => $period_key,
				'view_through_revenue' => $revenue_by_type['impression'],
				'conversion_through_revenue' => $revenue_by_type['conversion']
			];
		}
		
		return $formatted_results;
	}

	/**
	 * Calculate revenue grouped by time period or entity
	 * Unified method that handles both time-based and entity-based grouping
	 */
	public function calculateRevenueGrouped($results, $options = [])
	{
		// Handle empty results with proper zero-fill for time periods
		if (empty($results))
		{
			return $this->getEmptyPeriodData();
		}
		
		$orderMap = $this->buildOrderAttributionMap($results, function($result)
		{
			return $result->label; // Group by label (period or entity)
		});
		
		$chartData = [];
		$metadata = []; // Store additional fields like id
		
		// Build metadata from first occurrence of each item
		foreach ($results as $result)
		{
			$key = $result->label;
			if (!isset($metadata[$key]))
			{
				$metadata[$key] = (object) ['label' => $key];
				if (isset($result->id))
				{
					$metadata[$key]->id = intval($result->id);
				}
			}
		}
		
		// Calculate revenue for each item
		foreach ($orderMap as $key => $orders)
		{
			$total_revenue = 0.0;
			
			foreach ($orders as $orderData)
			{
				$total_revenue += $this->calculateAttributedRevenue($orderData);
			}
			
			$item = clone $metadata[$key];
			$item->total = round($total_revenue, 2);
			
			// For entity breakdowns, only include items with revenue > 0
			$isEntityBreakdown = in_array($this->type, ['top_campaign', 'countries', 'referrers', 'devices', 'pages', 'day_of_week']);
			if (!$isEntityBreakdown || $total_revenue > 0)
			{
				$chartData[] = $item;
			}
		}
		
		// Sort results by revenue total descending for entity breakdown types
		$isEntityBreakdown = in_array($this->type, ['top_campaign', 'countries', 'referrers', 'devices', 'pages', 'day_of_week']);
		if ($isEntityBreakdown)
		{
			usort($chartData, function($a, $b)
			{
				return $b->total <=> $a->total; // Sort descending by total revenue
			});
		}
		
		// Apply limit after sorting for breakdown types
		if ($isEntityBreakdown && $this->limit && is_numeric($this->limit) && $this->limit > 0)
		{
			$chartData = array_slice($chartData, 0, $this->limit);
		}
		
		return $chartData;
	}

	/**
	 * Generate empty data structure for periods with no revenue
	 */
	private function getEmptyPeriodData()
	{
		// For top_campaign type, return empty array since we can't generate meaningful empty data without campaign info
		if ($this->type === 'top_campaign')
		{
			return [];
		}
		
		// For single day, return 24 hours of zeros
		if ($this->isSingleDay())
		{
			$chartData = [];
			for ($hour = 0; $hour < 24; $hour++)
			{
				$hourLabel = sprintf('%02d:00', $hour);
				$chartData[] = (object) [
					'label' => $hourLabel,
					'total' => 0.0
				];
			}
			return $chartData;
		}
		
		return [];
	}

	/**
	 * Get the total for a specific order
	 */
	protected function getOrderTotal($order_id, $order_type)
	{
		if (empty($order_id))
		{
			return 0.0;
		}
		
		// Get the base order total using the shared helper
		return \FireBox\Core\RevenueAttribution\OrderHelper::getOrderTotal($order_id, $order_type);
	}
	
	/**
	 * Calculate attribution percentage based on attribution types for an order
	 * Multi-touch model: Split revenue equally among all attributed campaigns
	 * 
	 * @param array $attribution_types Array of attribution types for THIS campaign
	 * @param int $conversion_count Global count of all conversions for this order
	 * @param int $impression_count Global count of all impressions for this order
	 * @return float Attribution percentage to apply
	 */
	private function calculateAttributionPercentage($attribution_types, $conversion_count, $impression_count)
	{
		// Multi-touch model: 
		// If order has conversions → split among ALL conversions (conversion-through revenue)
		// If order has no conversions → split among ALL impressions (view-through revenue)
		if ($conversion_count > 0 && in_array('conversion', $attribution_types))
		{
			// This campaign contributed a conversion, split among all conversions
			return 1.0 / $conversion_count;
		}
		
		// This campaign only contributed an impression (or order has no conversions)
		return $impression_count > 0 ? 1.0 / $impression_count : 1.0;
	}
	
	/**
	 * Determines whether attribution percentage should be applied based on context
	 * 
	 * @return bool
	 */
	protected function shouldApplyAttribution()
	{
		// Always apply attribution when a specific campaign filter is set
		if ($this->hasCampaignFilter())
		{
			return true;
		}
		
		// Apply attribution for comparative breakdowns (comparing campaigns/segments)
		if ($this->isComparativeBreakdown())
		{
			return true;
		}
		
		// Don't apply attribution for aggregate/total views and attribution breakdowns without filters
		return false;
	}
	
	/**
	 * Check if a campaign filter is applied
	 * 
	 * @return bool
	 */
	private function hasCampaignFilter()
	{
		return array_key_exists('campaign', $this->filters) && 
			   isset($this->filters['campaign']['value']) && 
			   is_array($this->filters['campaign']['value']) && 
			   !empty($this->filters['campaign']['value']);
	}
	
	/**
	 * Check if this is a comparative breakdown that requires attribution
	 * 
	 * @return bool
	 */
	private function isComparativeBreakdown()
	{
		// These views compare different entities and need fair attribution
		$comparativeTypes = [
			'top_campaign',      // Comparing campaigns
			'countries',         // Comparing countries  
			'referrers',         // Comparing referrers
			'devices',           // Comparing devices
			'pages',             // Comparing pages
			'day_of_week'        // Comparing days
		];
		
		return in_array($this->type, $comparativeTypes);
	}
}
