<?php
/**
 * Dashboard service for computing admin dashboard data and metrics.
 *
 * @package RewardsWP
 */

namespace Awesomemotive\Rewardswp\Services;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

use Awesomemotive\Rewardswp\Factories\IntegrationsFactory;
use Awesomemotive\Rewardswp\Interfaces\IntegrationInterface;
use Awesomemotive\Rewardswp\Repositories\DashboardRepository;
use Awesomemotive\Rewardswp\Settings\Options\Points\PointsEnabledOption;
use Awesomemotive\Rewardswp\Settings\Options\Referrals\EnabledOption as ReferralsEnabledOption;
use function Awesomemotive\Rewardswp\Helpers\get_human_readable_time_ago;
use function Awesomemotive\Rewardswp\Helpers\get_option_value;
use function Awesomemotive\Rewardswp\Helpers\format_currency;
use function Awesomemotive\Rewardswp\Helpers\get_reward_display_title_from_values;

/**
 * Dashboard service for computing admin dashboard data and metrics.
 *
 * @since 1.0.0
 *
 * @param DashboardRepository $dashboard_repository Dashboard repository instance.
 * @param PanelService        $panel_service        Panel service instance.
 */
class DashboardService {

	/**
	 * Dashboard repository instance.
	 *
	 * @since 1.1.0
	 *
	 * @var DashboardRepository
	 */
	protected DashboardRepository $dashboard_repository;

	/**
	 * Panel service instance.
	 *
	 * @since 1.1.0
	 *
	 * @var PanelService
	 */
	private PanelService $panel_service;

	/**
	 * Integration interface instance.
	 *
	 * @since 1.1.0
	 *
	 * @var IntegrationInterface
	 */
	private IntegrationInterface $integration;

	/**
	 * Integrations factory instance.
	 *
	 * @since 1.1.0
	 *
	 * @var IntegrationsFactory
	 */
	private IntegrationsFactory $integrations_factory;

	/**
	 * Constructor.
	 *
	 * @since 1.1.0
	 *
	 * @param DashboardRepository  $dashboard_repository  Dashboard repository instance.
	 * @param PanelService         $panel_service         Panel service instance.
	 * @param IntegrationInterface $integration           Integration interface instance.
	 * @param IntegrationsFactory  $integrations_factory  Integrations factory instance.
	 */
	public function __construct(
		DashboardRepository $dashboard_repository,
		PanelService $panel_service,
		IntegrationInterface $integration,
		IntegrationsFactory $integrations_factory
	) {
		$this->dashboard_repository  = $dashboard_repository;
		$this->panel_service         = $panel_service;
		$this->integration           = $integration;
		$this->integrations_factory  = $integrations_factory;
	}

	/**
	 * Get KPI card definitions with metadata.
	 * This defines the structure and properties of each KPI card.
	 *
	 * @since 1.0.0
	 * @return array Array of KPI card definitions
	 */
	public function get_kpi_definitions(): array {
		return [
			'rewards-applied-revenue' => [
				'id'          => 'rewards-applied-revenue',
				'title'       => __( 'Rewards-applied revenue', 'rewardswp' ),
				'tooltip'     => [
					__( 'Net sales from orders in this period that used a RewardsWP incentive (points redemption or referral reward), excluding tax and shipping and subtracting refunds.', 'rewardswp' ),
					__( 'This shows how much revenue your rewards and referrals are directly influencing.', 'rewardswp' ),
				],
				'value_type'  => 'currency',
				'plans'       => [ 'lite', 'basic', 'plus', 'pro' ],
				'priority'    => 10,
				'sparkline'   => [
					'enabled' => true,
					'type'    => 'line',
					'period'  => 'last_12_weeks',
				],
				'delta'       => [
					'show'       => true,
					'compare_to' => 'previous_period',
				],
				'meta_row'    => [
					'show'   => true,
					'fields' => [ 'discounts_given', 'refunds' ],
				],
				'badge'       => [
					'type'        => 'multiple',
					'calculation' => 'rewards_revenue / discount_cost',
					'thresholds'  => [
						[
							'min'    => 7,
							'status' => 'healthy',
							'label'  => __( 'Healthy', 'rewardswp' ),
						],
						[
							'min'    => 4,
							'max'    => 7,
							'status' => 'okay',
							'label'  => __( 'Okay', 'rewardswp' ),
						],
						[
							'max'    => 4,
							'status' => 'attention',
							'label'  => __( 'Attention', 'rewardswp' ),
						],
					],
				],
				'empty_state' => [
					'condition'   => 'value === 0',
					'threshold'   => [
						'min_orders'  => 5,
						'min_revenue' => 500,
						'period'      => 'last_30_days',
					],
					'empty_value' => format_currency( 0 ),
					'empty_delta' => '',
					'message'     => __( 'Not enough data yet', 'rewardswp' ),
					'description' => __( 'When more orders use rewards, this chart will show the revenue they drive.', 'rewardswp' ),
					'actions'     => [],
				],
				'featured'    => [
					'eligible'   => true,
					'conditions' => [
						'min_rewards_orders'    => 5,
						'min_rewards_revenue'   => 500,
						'days_since_activation' => 14,
					],
					'priority'   => 1,
				],
			],

			'new-members'             => [
				'id'          => 'new-members',
				'title'       => __( 'New members', 'rewardswp' ),
				'tooltip'     => __( 'People who joined your rewards program in this period. Growing this number builds your future base of engaged members and revenue.', 'rewardswp' ),
				'value_type'  => 'number',
				'plans'       => [ 'lite', 'basic', 'plus', 'pro' ],
				'priority'    => 20,
				'sparkline'   => [
					'enabled' => false,
					'type'    => 'bar',
				],
				'subline'     => [
					'show'  => true,
					'text'  => __( 'Total members: {total}', 'rewardswp' ),
					'style' => 'muted',
				],
				'delta'       => [
					'show'       => true,
					'compare_to' => 'previous_30_days',
				],
				'empty_state' => [
					'cases' => [
						'no_members'  => [
							'condition'    => 'total_members === 0',
							'empty_value'  => '0',
							'empty_delta'  => '',
							'show_subline' => false,
							'message'      => __( 'No new members yet', 'rewardswp' ),
							'description'  => __( 'When new members join your rewards program, they\'ll appear here.', 'rewardswp' ),
						],
						'no_activity' => [
							'condition'    => 'value === 0',
							'empty_value'  => '0',
							'empty_delta'  => '',
							'show_subline' => true,
							'message'      => __( 'No new members this period', 'rewardswp' ),
							'description'  => '',
						],
					],
				],
				'featured'    => [
					'eligible'   => true,
					'conditions' => [
						'is_default' => true,
					],
					'priority'   => 2,
				],
			],

			'active-members'          => [
				'id'             => 'active-members',
				'title'          => __( 'Active members', 'rewardswp' ),
				'tooltip'        => __( 'Members who earned or redeemed rewards at least once in this period. A higher active share means members are regularly engaging with your program.', 'rewardswp' ),
				'value_type'     => 'percentage_with_count',
				'display_format' => '{percentage}% ({active} / {total})',
				'plans'          => [ 'lite', 'basic', 'plus', 'pro' ],
				'priority'       => 30,
				'sparkline'      => [
					'enabled' => false,
					'type'    => 'line',
				],
				'delta'          => [
					'show'       => true,
					'compare_to' => 'previous_period',
					'type'       => 'percentage_points',
				],
				'badge'          => [
					'type'       => 'health',
					'thresholds' => [
						[
							'min'    => 30,
							'status' => 'healthy',
							'label'  => __( 'Healthy', 'rewardswp' ),
						],
						[
							'min'    => 15,
							'max'    => 30,
							'status' => 'okay',
							'label'  => __( 'Okay', 'rewardswp' ),
						],
						[
							'max'    => 15,
							'status' => 'attention',
							'label'  => __( 'Attention', 'rewardswp' ),
						],
					],
				],
				'empty_state'    => [
					'condition'    => 'value === 0',
					'empty_value'  => '0%',
					'empty_delta'  => '',
					'show_subline' => false,
					'message'      => __( 'No active members this period', 'rewardswp' ),
				],
			],

			'points-activity'         => [
				'id'               => 'points-activity',
				'title'            => __( 'Points earned/redeemed', 'rewardswp' ),
				'tooltip'          => __( 'Total points earned vs redeemed in this period, plus the redemption rate. A higher redemption rate means members find rewards valuable and are coming back to use them.', 'rewardswp' ),
				'value_type'       => 'dual_value_with_rate',
				'display_format'   => '{earned} / {redeemed}',
				'subline'          => [
					'show'        => true,
					'text'        => __( 'RR: {rate}%', 'rewardswp' ),
					'delta_field' => 'rr_delta',
					'style'       => 'muted',
				],
				'plans'            => [ 'lite', 'basic', 'plus', 'pro' ],
				'priority'         => 40,
				'requires_feature' => 'points',
				'sparkline'        => [
					'enabled' => false,
					'type'    => 'line',
				],

				'badge'            => [
					'type'       => 'redemption_rate',
					'thresholds' => [
						[
							'min'    => 40,
							'status' => 'healthy',
							'label'  => __( 'Healthy', 'rewardswp' ),
						],
						[
							'min'    => 20,
							'max'    => 40,
							'status' => 'okay',
							'label'  => __( 'Okay', 'rewardswp' ),
						],
						[
							'max'    => 20,
							'status' => 'attention',
							'label'  => __( 'Attention', 'rewardswp' ),
						],
					],
				],
				'empty_state'      => [
					'cases' => [
						'feature_disabled_no_data' => [
							'condition'    => '!feature_enabled && points_earned === 0 && points_redeemed === 0',
							'empty_value'  => '',
							'empty_delta'  => '',
							'show_subline' => false,
							'show_badge'   => true,
							'badge_text'   => __( 'Program off', 'rewardswp' ),
							'message'      => '',
							'description'  => __( 'Turn them on to build repeat purchases.', 'rewardswp' ),
							'actions'      => [
								[
									'label' => __( 'Enable points', 'rewardswp' ),
									'url'   => 'admin.php?page=rewardswp-settings&tab=points#points-status',
								],
							],
						],
						'no_activity'              => [
							'condition'    => 'points_earned === 0 && points_redeemed === 0',
							'empty_value'  => '0 / 0',
							'empty_delta'  => '',
							'show_subline' => false,
							'message'      => __( 'No points activity yet', 'rewardswp' ),
							'description'  => __( 'When members earn or redeem points, totals will appear here.', 'rewardswp' ),
						],
					],
				],
				'program_status'   => [
					'disabled_badge'   => __( 'Program off', 'rewardswp' ),
					'disabled_message' => __( 'Your points data still appears here. Turn points back on to let members earn and redeem again.', 'rewardswp' ),
					'action'           => [
						'label' => __( 'Enable points', 'rewardswp' ),
						'url'   => 'admin.php?page=rewardswp-settings&tab=points#points-status',
					],
				],
			],

			'completed-referrals'     => [
				'id'               => 'completed-referrals',
				'title'            => __( 'Completed referrals', 'rewardswp' ),
				'tooltip'          => __( 'New customers whose first purchase in this period came from a referral. This shows how many new buyers your referral program is adding.', 'rewardswp' ),
				'value_type'       => 'number',
				'plans'            => [ 'lite', 'basic', 'plus', 'pro' ],
				'priority'         => 50,
				'requires_feature' => 'referrals',
				'sparkline'        => [
					'enabled' => false,
					'type'    => 'bar',
				],
				'delta'            => [
					'show'       => true,
					'compare_to' => 'previous_period',
				],
				'empty_state'      => [
					'cases' => [
						'feature_disabled_no_data' => [
							'condition'    => '!feature_enabled && value === 0',
							'empty_value'  => '',
							'empty_delta'  => '',
							'show_subline' => false,
							'show_badge'   => true,
							'badge_text'   => __( 'Program off', 'rewardswp' ),
							'message'      => '',
							'description'  => __( 'Turn them on to unlock word-of-mouth sales.', 'rewardswp' ),
							'actions'      => [
								[
									'label' => __( 'Enable referrals', 'rewardswp' ),
									'url'   => 'admin.php?page=rewardswp-settings&tab=referrals#program-status',
								],
							],
						],
						'no_activity'              => [
							'condition'    => 'value === 0',
							'empty_value'  => '0',
							'empty_delta'  => '',
							'show_subline' => false,
							'message'      => __( 'No completed referrals yet', 'rewardswp' ),
							'description'  => __( 'When referred friends purchase, they\'ll appear here.', 'rewardswp' ),
						],
					],
				],
				'program_status'   => [
					'disabled_badge'   => __( 'Program off', 'rewardswp' ),
					'disabled_message' => __( 'Your referral data still appears here. Turn referrals back on to continue growing through word-of-mouth.', 'rewardswp' ),
					'action'           => [
						'label' => __( 'Enable referrals', 'rewardswp' ),
						'url'   => 'admin.php?page=rewardswp-settings&tab=referrals#program-status',
					],
				],
			],

			'orders-using-rewards'    => [
				'id'             => 'orders-using-rewards',
				'title'          => __( 'Orders using rewards', 'rewardswp' ),
				'tooltip'        => __( 'Orders in this period where a RewardsWP incentive (points redemption or referral reward) was applied at checkout. A higher share means rewards are influencing more of your sales.', 'rewardswp' ),
				'value_type'     => 'percentage_with_count',
				'display_format' => '{percentage}% ({rewarded} / {total})',
				'plans'          => [ 'basic', 'plus', 'pro' ],
				'priority'       => 60,
				'sparkline'      => [
					'enabled' => false,
					'type'    => 'line',
				],
				'delta'          => [
					'show'       => true,
					'compare_to' => 'previous_period',
					'type'       => 'percentage_points',
				],
				'empty_state'    => [
					'condition'   => 'value === 0',
					'empty_value' => '0%',
					'empty_delta' => '',
					'message'     => __( 'No orders used rewards this period', 'rewardswp' ),
				],
			],

			'first-time-redeemers'    => [
				'id'          => 'first-time-redeemers',
				'title'       => __( 'First-time redeemers', 'rewardswp' ),
				'tooltip'     => __( 'Members who redeemed a reward for the first time in this period. This shows how many people are reaching the ‘first reward’ milestone and experiencing the value of your program.', 'rewardswp' ),
				'value_type'  => 'number',
				'plans'       => [ 'basic', 'plus', 'pro' ],
				'priority'    => 45, // Places it between points-activity (40) and completed-referrals (50).
				'sparkline'   => [
					'enabled' => false,
					'type'    => 'bar',
				],
				'subline'     => [
					'show'  => true,
					'text'  => __( 'Median time to first: {median_time}', 'rewardswp' ),
					'style' => 'muted',
				],
				'delta'       => [
					'show'       => true,
					'compare_to' => 'previous_30_days',
				],
				'badge'       => [
					'type'       => 'health',
					'thresholds' => [
						[
							'min'    => 10,
							'status' => 'healthy',
							'label'  => __( 'Healthy', 'rewardswp' ),
						],
						[
							'min'    => 3,
							'max'    => 10,
							'status' => 'okay',
							'label'  => __( 'Okay', 'rewardswp' ),
						],
						[
							'max'    => 3,
							'status' => 'attention',
							'label'  => __( 'Attention', 'rewardswp' ),
						],
					],
				],
				'empty_state' => [
					'condition'    => 'value === 0',
					'empty_value'  => '0',
					'empty_delta'  => '',
					'show_subline' => false,
					'message'      => __( 'No first-time redeemers this period', 'rewardswp' ),
				],
			],
		];
	}

	/**
	 * Get KPI data for a specific date range.
	 * Returns the actual values for KPIs.
	 *
	 * @since 1.0.0
	 *
	 * @param string $range The date range (e.g., 'last_30_days').
	 *
	 * @return array KPI data with values.
	 */
	public function get_kpi_data( string $range = 'last_30_days' ): array {
		// Fetch real data from database.
		$data = $this->fetch_kpi_data( $range );

		// Apply filter to allow dev helper or other plugins to modify.
		return apply_filters( 'rewardswp_dashboard_kpi_data', $data, $range );
	}

	/**
	 * Fetch KPI data from database.
	 * Private method that does the actual data fetching.
	 *
	 * @since 1.0.0
	 * @param string $range The date range.
	 * @return array KPI data
	 */
	private function fetch_kpi_data( string $range ): array {
		$data = [];

		// Calculate each KPI's data.
		$data['rewards-applied-revenue'] = $this->calculate_rewards_revenue( $range );
		$data['new-members']             = $this->calculate_new_members( $range );
		$data['active-members']          = $this->calculate_active_members( $range );
		$data['points-activity']         = $this->calculate_points_activity( $range );
		$data['first-time-redeemers']    = $this->calculate_first_time_redeemers( $range );
		$data['completed-referrals']     = $this->calculate_completed_referrals( $range );
		$data['orders-using-rewards']    = $this->calculate_orders_using_rewards( $range );

		return $data;
	}

	/**
	 * Calculate rewards-applied revenue using real database data.
	 *
	 * Returns net revenue from product sales influenced by RewardsWP (orders with
	 * referrals or reward redemptions), minus refunds, with comparison to previous period.
	 *
	 * @since 1.0.0
	 * @param string $range The date range.
	 * @return array Revenue data.
	 */
	private function calculate_rewards_revenue( string $range ): array {
		// Get date boundaries for current and previous periods.
		$boundaries = $this->get_date_boundaries( $range );

		// Calculate revenue using repository.
		$revenue_data = $this->calculate_rewards_applied_revenue(
			$boundaries['start'],
			$boundaries['end'],
			$boundaries['prev_start'],
			$boundaries['prev_end']
		);

		// Prepare data structure for dashboard.
		$data = [
			'value'       => $revenue_data['value_raw'],
			'value_html'  => $revenue_data['value'],
			'delta'       => $revenue_data['change_percentage'],
			'delta_value' => 0, // Not used, but kept for compatibility.
			'delta_class' => $this->get_delta_class_from_badge( $revenue_data['badge'] ),
			'meta'        => [
				'discounts_given' => (float) ( $revenue_data['discounts_raw'] ?? 0 ),
				'refunds'         => (float) ( $revenue_data['refunds_raw'] ?? 0 ),
			],
			'qualified'   => true,
		];

		// Add sparkline if enabled in KPI definition.
		$kpi_def = $this->get_kpi_definitions()['rewards-applied-revenue'] ?? [];
		if ( ! empty( $kpi_def['sparkline']['enabled'] ) && true === $kpi_def['sparkline']['enabled'] ) {
			// Format sparkline data for ApexCharts.
			$sparkline_data = [];
			foreach ( $revenue_data['sparkline'] as $point ) {
				$sparkline_data[] = [
					'x'       => $point['date'], // ISO date for x-axis.
					'y'       => $point['value'], // Net revenue for y-axis.
					'refunds' => $point['refunds'], // Refunds for tooltip.
					'date'    => gmdate( 'D, j M', strtotime( $point['date'] ) ), // Formatted date for display.
					'dateISO' => $point['date'], // ISO date for reference.
				];
			}

			$data['sparkline'] = $sparkline_data;
		}

		return $data;
	}

	/**
	 * Get delta CSS class from badge status.
	 *
	 * @since 1.0.0
	 * @param string $badge Badge status ('healthy', 'warning', 'critical').
	 * @return string CSS class for delta display.
	 */
	private function get_delta_class_from_badge( string $badge ): string {
		switch ( $badge ) {
			case 'healthy':
				return 'text-emerald-600';
			case 'warning':
				return 'text-amber-600';
			case 'critical':
				return 'text-red-600';
			default:
				return 'text-gray-600';
		}
	}

	/**
	 * Calculate new members.
	 *
	 * @since 1.0.0
	 *
	 * @param string $range The date range.
	 *
	 * @return array Members data.
	 */
	private function calculate_new_members( string $range ): array {
		// Get date boundaries for current and previous periods.
		$dates = $this->get_date_boundaries( $range );

		// Query new members count for current period.
		$new_members_count = $this->dashboard_repository->get_new_members_count(
			$dates['start'],
			$dates['end']
		);

		// Query total members count.
		$total_members = $this->dashboard_repository->get_total_members_count();

		// Query previous period for delta calculation.
		$previous_count = $this->dashboard_repository->get_new_members_count(
			$dates['prev_start'],
			$dates['prev_end']
		);

		// Calculate delta.
		$delta_value   = $new_members_count - $previous_count;
		$delta_percent = $previous_count > 0
			? round( ( $delta_value / $previous_count ) * 100, 0 )
			: ( $new_members_count > 0 ? 100 : 0 );

		$delta_class = $delta_value >= 0 ? 'text-emerald-600' : 'text-red-600';
		$delta_sign  = $delta_value >= 0 ? '+' : '';

		$data = [
			'value'         => $new_members_count,
			'value_html'    => (string) $new_members_count,
			'total_members' => $total_members,
			'delta'         => $delta_sign . $delta_percent . '%',
			'delta_value'   => $delta_value,
			'delta_class'   => $delta_class,
		];

		return $data;
	}

	/**
	 * Calculate active members percentage.
	 *
	 * @since 1.0.0
	 *
	 * @param string $range The date range.
	 *
	 * @return array Active members data
	 */
	private function calculate_active_members( string $range ): array {
		$dates = $this->get_date_boundaries( $range );

		// Get counts for current period.
		$active_count = $this->dashboard_repository->get_active_members_count( $dates['start'], $dates['end'] );
		$total_count  = $this->dashboard_repository->get_total_members_count();

		// Get counts for previous period.
		$prev_active_count = $this->dashboard_repository->get_active_members_count( $dates['prev_start'], $dates['prev_end'] );
		$prev_total_count  = $total_count; // Total doesn't change for delta calculation.

		// Calculate percentages.
		$percentage      = $total_count > 0 ? round( ( $active_count / $total_count ) * 100, 1 ) : 0;
		$prev_percentage = $prev_total_count > 0 ? round( ( $prev_active_count / $prev_total_count ) * 100, 1 ) : 0;

		// Calculate delta in percentage points (not percentage change).
		$delta_value = round( $percentage - $prev_percentage, 1 );
		$delta_sign  = $delta_value > 0 ? '+' : '';
		$delta_class = $delta_value > 0 ? 'text-emerald-600' : ( $delta_value < 0 ? 'text-red-600' : 'text-gray-600' );

		$data = [
			'value'        => $percentage,
			'value_html'   => sprintf( '%s%% (%d / %d)', $percentage, $active_count, $total_count ),
			'percentage'   => $percentage,
			'active_count' => $active_count,
			'total_count'  => $total_count,
			'delta'        => $delta_sign . $delta_value . ' pp',
			'delta_value'  => $delta_value,
			'delta_class'  => $delta_class,
		];

		// Sparkline for active members is intentionally disabled.
		$kpi_def = $this->get_kpi_definitions()['active-members'] ?? [];
		if ( ! empty( $kpi_def['sparkline']['enabled'] ) && true === $kpi_def['sparkline']['enabled'] ) {
			$data['sparkline'] = [];
		}

		return $data;
	}

	/**
	 * Calculate points activity (earned vs redeemed + RR).
	 *
	 * @since 1.0.0
	 * @param string $range The date range.
	 * @return array Points activity data.
	 */
	private function calculate_points_activity( string $range ): array {
		$dates = $this->get_date_boundaries( $range );

		// Get counts for current period.
		$points_earned   = $this->dashboard_repository->get_points_earned( $dates['start'], $dates['end'] );
		$points_redeemed = $this->dashboard_repository->get_points_redeemed( $dates['start'], $dates['end'] );

		// Get counts for previous period.
		$prev_points_earned   = $this->dashboard_repository->get_points_earned( $dates['prev_start'], $dates['prev_end'] );
		$prev_points_redeemed = $this->dashboard_repository->get_points_redeemed( $dates['prev_start'], $dates['prev_end'] );

		// Calculate Redemption Rate (RR).
		$raw_rr          = $points_earned > 0 ? ( $points_redeemed / $points_earned ) * 100 : 0;
		$redemption_rate = round( $raw_rr, 1 );
		$prev_raw_rr     = $prev_points_earned > 0 ? ( $prev_points_redeemed / $prev_points_earned ) * 100 : 0;
		$prev_rr         = round( $prev_raw_rr, 1 );

		// Display "<0.1" when there are redemptions but rate rounds to 0.
		$rate_display = $redemption_rate;
		if ( 0.0 === $redemption_rate && $points_redeemed > 0 && $points_earned > 0 ) {
			$rate_display = '<0.1';
		}

		// Calculate RR delta in percentage points.
		$rr_delta       = round( $redemption_rate - $prev_rr, 1 );
		$rr_delta_sign  = $rr_delta > 0 ? '+' : '';
		$rr_delta_class = $rr_delta > 0 ? 'text-emerald-600' : ( $rr_delta < 0 ? 'text-red-600' : 'text-gray-600' );

		$data = [
			'value'           => number_format( $points_earned ) . ' / ' . number_format( $points_redeemed ),
			'value_html'      => '<span>' . number_format( $points_earned ) . '</span> / <span>' . number_format( $points_redeemed ) . '</span>',
			'points_earned'   => $points_earned,
			'points_redeemed' => $points_redeemed,
			'redemption_rate' => $redemption_rate,
			'rate'            => $rate_display,
			'rr_delta'        => $rr_delta_sign . $rr_delta . ' pp',
			'rr_delta_class'  => $rr_delta_class,
			'feature_enabled' => (bool) get_option_value( PointsEnabledOption::class ),
		];

		// Sparkline for points activity is intentionally disabled.
		$kpi_def = $this->get_kpi_definitions()['points-activity'] ?? [];
		if ( ! empty( $kpi_def['sparkline']['enabled'] ) && true === $kpi_def['sparkline']['enabled'] ) {
			$data['sparkline'] = [];
		}

		return $data;
	}

	/**
	 * Calculate first-time redeemers KPI.
	 *
	 * Lite stub: First-time redeemers KPI is a Pro feature.
	 * Returns default data structure for Lite users.
	 *
	 * @since 1.1.0
	 * @param string $range The date range.
	 * @return array First-time redeemers data.
	 */
	protected function calculate_first_time_redeemers( string $range ): array {
		return [
			'value'       => 0,
			'value_html'  => '0',
			'median_time' => __( 'N/A', 'rewardswp' ),
			'delta'       => '+0%',
			'delta_value' => 0,
			'delta_class' => 'text-emerald-600',
			'badge'       => [
				'status' => 'attention',
			],
		];
	}

	/**
	 * Calculate completed referrals.
	 *
	 * @since 1.0.0
	 *
	 * @param string $range The date range.
	 *
	 * @return array Referrals data.
	 */
	private function calculate_completed_referrals( string $range ): array {
		// Get date boundaries for current and previous periods.
		$dates = $this->get_date_boundaries( $range );

		// Query completed referrals count for current period.
		$current_count = $this->dashboard_repository->get_referrals_count_by_status(
			$dates['start'],
			$dates['end'],
			'completed'
		);

		// Query previous period for delta calculation.
		$previous_count = $this->dashboard_repository->get_referrals_count_by_status(
			$dates['prev_start'],
			$dates['prev_end'],
			'completed'
		);

		// Calculate delta.
		$delta_value   = $current_count - $previous_count;
		$delta_percent = $previous_count > 0
			? round( ( $delta_value / $previous_count ) * 100, 0 )
			: ( $current_count > 0 ? 100 : 0 );

		$delta_class = $delta_value >= 0 ? 'text-emerald-600' : 'text-red-600';
		$delta_sign  = $delta_value >= 0 ? '+' : '';

		$data = [
			'value'           => $current_count,
			'value_html'      => (string) $current_count,
			'delta'           => $delta_sign . $delta_percent . '%',
			'delta_value'     => $delta_value,
			'delta_class'     => $delta_class,
			'feature_enabled' => (bool) get_option_value( ReferralsEnabledOption::class ),
		];

		// Only add sparkline if enabled in KPI definition.
		$kpi_def = $this->get_kpi_definitions()['completed-referrals'] ?? [];
		if ( ! empty( $kpi_def['sparkline']['enabled'] ) && true === $kpi_def['sparkline']['enabled'] ) {
			// Get daily referrals data from repository.
			$daily_data = $this->dashboard_repository->get_referrals_by_day(
				$dates['start'],
				$dates['end'],
				'completed'
			);

			// Fill gaps to ensure we have all days (even with 0 counts).
			$days              = $this->get_days_for_range( $range );
			$data['sparkline'] = $this->fill_date_gaps( $daily_data, $days );
		}

		return $data;
	}

	/**
	 * Calculate orders using rewards KPI.
	 *
	 * Lite stub: Orders using rewards KPI is a Pro feature.
	 * Returns default data structure for Lite users.
	 *
	 * @since 1.1.0
	 * @param string $range The date range.
	 * @return array Orders using rewards data.
	 */
	protected function calculate_orders_using_rewards( string $range ): array {
		return [
			'value'          => 0,
			'value_html'     => '0%',
			'percentage'     => 0,
			'rewarded_count' => 0,
			'total_count'    => 0,
			'delta'          => '+0 pp',
			'delta_value'    => 0,
			'delta_class'    => 'text-emerald-600',
		];
	}

	/**
	 * Check if a feature is enabled.
	 *
	 * @since 1.0.0
	 * @param string $feature The feature to check ('points' or 'referrals').
	 * @return bool Whether the feature is enabled.
	 */
	private function is_feature_enabled( string $feature ): bool {
		// Use simple enabled checks for dashboard KPIs - we only want to show "Program off"
		// when the user has explicitly disabled the feature, not when it's missing configuration.
		switch ( $feature ) {
			case 'points':
				return (bool) get_option_value( PointsEnabledOption::class );
			case 'referrals':
				return (bool) get_option_value( ReferralsEnabledOption::class );
			default:
				return false;
		}
	}

	/**
	 * Get available date ranges for the dashboard.
	 *
	 * @since 1.0.0
	 * @return array Date range definitions
	 */
	public function get_date_ranges(): array {
		return [
			'last_7_days'  => [
				'label' => __( 'Last 7 days', 'rewardswp' ),
				'days'  => 7,
			],
			'last_14_days' => [
				'label' => __( 'Last 14 days', 'rewardswp' ),
				'days'  => 14,
			],
			'last_30_days' => [
				'label' => __( 'Last 30 days', 'rewardswp' ),
				'days'  => 30,
			],
			'last_60_days' => [
				'label' => __( 'Last 60 days', 'rewardswp' ),
				'days'  => 60,
			],
			'last_90_days' => [
				'label' => __( 'Last 90 days', 'rewardswp' ),
				'days'  => 90,
			],
		];
	}

	/**
	 * Get chart definitions for the dashboard.
	 * Charts are separate from KPIs and available for higher plans.
	 *
	 * @since 1.0.0
	 * @return array Chart definitions
	 */
	public function get_chart_definitions(): array {
		return [
			'member-growth'         => [
				'id'       => 'member-growth',
				'title'    => __( 'Member Growth', 'rewardswp' ),
				'type'     => 'line',
				'plans'    => [ 'basic', 'plus', 'pro' ],
				'priority' => 10,
				'size'     => 'half',
				'data_key' => 'member_growth',
			],
			'referral-revenue'      => [
				'id'               => 'referral-revenue',
				'title'            => __( 'Referral Revenue Over Time', 'rewardswp' ),
				'type'             => 'bar-line',  // Bar chart with line overlay.
				'plans'            => [ 'lite', 'basic', 'plus', 'pro' ],  // Added 'lite'.
				'priority'         => 20,
				'size'             => 'half',
				'data_key'         => 'referral_revenue',
				'requires_feature' => 'referrals',
				'summary_position' => 'top-right',  // Position for summary text.
				'series'           => [
					[
						'key'   => 'revenue',
						'label' => __( 'Referral revenue', 'rewardswp' ),
						'type'  => 'bar',
						'color' => '#3B82F6',
					],
					[
						'key'   => '7day_avg',
						'label' => __( '7-day avg', 'rewardswp' ),
						'type'  => 'line',
						'color' => '#94A3B8',
					],
				],
			],
			'points-activity-chart' => [
				'id'               => 'points-activity-chart',
				'title'            => __( 'Points Activity Over Time', 'rewardswp' ),
				'subtitle'         => __( 'RR: —', 'rewardswp' ),
				'type'             => 'multi-line',
				'plans'            => [ 'lite', 'basic', 'plus', 'pro' ],  // Added 'lite'.
				'priority'         => 30,
				'size'             => 'half',
				'data_key'         => 'points_activity',
				'requires_feature' => 'points',
				'badge_position'   => 'top-left',  // Position for RR badge.
				'series'           => [
					[
						'key'   => 'earned',
						'label' => __( 'Points earned', 'rewardswp' ),
						'color' => '#2563eb',
					],
					[
						'key'   => 'redeemed',
						'label' => __( 'Points redeemed', 'rewardswp' ),
						'color' => '#fb923c',
					],
				],
			],
		];
	}

	/**
	 * Get chart data for a specific chart and date range.
	 *
	 * @since 1.0.0
	 * @param string $chart_type The chart identifier.
	 * @param string $range The date range.
	 * @return array Chart data
	 */
	public function get_chart_data( string $chart_type, string $range = 'last_30_days' ): array {
		// Fetch real data from database.
		$data = $this->fetch_chart_data( $chart_type, $range );

		// Apply filter to allow dev helper or other plugins to modify.
		return apply_filters( 'rewardswp_dashboard_chart_data', $data, $chart_type, $range );
	}

	/**
	 * Fetch chart data from database.
	 *
	 * @since 1.0.0
	 * @param string $chart_type The chart identifier.
	 * @param string $range The date range.
	 * @return array Chart data.
	 */
	private function fetch_chart_data( string $chart_type, string $range ): array {
		switch ( $chart_type ) {
			case 'referral-revenue':
				return $this->fetch_referral_revenue_chart_data( $range );

			case 'points-activity-chart':
				return $this->fetch_points_activity_chart_data( $range );

			case 'member-growth':
				return $this->fetch_member_growth_chart_data( $range );

			default:
				// Return empty structure for unknown chart types.
				$days   = $this->get_date_ranges()[ $range ]['days'] ?? 30;
				$labels = [];

				// Generate date labels.
				for ( $i = $days - 1; $i >= 0; $i-- ) {
					$labels[] = gmdate( 'Y-m-d', strtotime( "-{$i} days" ) );
				}

				return [
					'labels'   => $labels,
					'datasets' => [],
				];
		}
	}

	/**
	 * Fetch referral revenue chart data using real database data.
	 * Shows daily referral revenue bars with 7-day moving average line.
	 *
	 * @since 1.0.0
	 * @param string $range The date range.
	 * @return array Chart data for referral revenue.
	 */
	private function fetch_referral_revenue_chart_data( string $range ): array {
		// Get date boundaries for current and previous periods.
		$boundaries = $this->get_date_boundaries( $range );

		// Get total revenue for current and previous periods.
		$current_data  = $this->dashboard_repository->get_referral_revenue_total( $boundaries['start'], $boundaries['end'] );
		$previous_data = $this->dashboard_repository->get_referral_revenue_total( $boundaries['prev_start'], $boundaries['prev_end'] );

		// Get daily revenue data.
		$daily_data = $this->dashboard_repository->get_referral_revenue_by_day( $boundaries['start'], $boundaries['end'] );

		// Prepare arrays for chart.
		$labels        = [];
		$daily_revenue = [];
		$moving_avg    = [];

		// If no data, generate empty arrays for the date range.
		if ( empty( $daily_data ) ) {
			$days = $this->get_days_for_range( $range );
			for ( $i = $days - 1; $i >= 0; $i-- ) {
				$date            = gmdate( 'Y-m-d', strtotime( "-{$i} days" ) );
				$labels[]        = gmdate( 'M j', strtotime( $date ) );
				$daily_revenue[] = 0;
				$moving_avg[]    = 0;
			}
		} else {
			// Extract daily net revenue values and generate labels.
			foreach ( $daily_data as $date => $data ) {
				$labels[]        = gmdate( 'M j', strtotime( $date ) );
				$daily_revenue[] = $data['net'];
			}
		}

		// Calculate 7-day moving average (only if we have data).
		if ( ! empty( $daily_data ) ) {
			$daily_revenue_count = count( $daily_revenue );
			for ( $i = 0; $i < $daily_revenue_count; $i++ ) {
				$start        = max( 0, $i - 6 );
				$end          = $i + 1;
				$window       = array_slice( $daily_revenue, $start, $end - $start );
				$avg          = count( $window ) > 0 ? array_sum( $window ) / count( $window ) : 0;
				$moving_avg[] = round( $avg, 2 );
			}
		}

		// Calculate period totals and % change.
		$current_total  = $current_data['net_revenue'];
		$previous_total = $previous_data['net_revenue'];

		$delta_percent = 0.0;
		if ( $previous_total > 0 ) {
			$delta_percent = ( ( $current_total - $previous_total ) / $previous_total ) * 100;
		} elseif ( $current_total > 0 ) {
			$delta_percent = 100.0; // From 0 to positive = 100%.
		}

		return [
			'labels'          => $labels,
			'datasets'        => [
				[
					'name'  => __( 'Referral revenue', 'rewardswp' ),
					'type'  => 'bar',
					'data'  => $daily_revenue,
					'color' => '#3B82F6',
				],
				[
					'name'  => __( '7-day avg', 'rewardswp' ),
					'type'  => 'line',
					'data'  => $moving_avg,
					'color' => '#94A3B8',
				],
			],
			'summary'         => [
				'total'           => $current_total,
				'total_formatted' => format_currency( $current_total ),
				'delta'           => ( $delta_percent > 0 ? '+' : '' ) . number_format( $delta_percent, 1 ) . '%',
				'delta_type'      => $delta_percent >= 0 ? 'positive' : 'negative',
			],
			'feature_enabled' => $this->is_feature_enabled( 'referrals' ),
		];
	}

	/**
	 * Fetch points activity chart data using real database data.
	 * Shows points earned vs redeemed over time with redemption rate.
	 *
	 * @since 1.0.0
	 * @param string $range The date range.
	 * @return array Chart data for points activity.
	 */
	private function fetch_points_activity_chart_data( string $range ): array {
		// Get date boundaries for current and previous periods.
		$boundaries = $this->get_date_boundaries( $range );
		$days       = $this->get_days_for_range( $range );

		// Get daily earned and redeemed data from database.
		$earned_by_day   = $this->dashboard_repository->get_points_earned_by_day( $boundaries['start'], $boundaries['end'] );
		$redeemed_by_day = $this->dashboard_repository->get_points_redeemed_by_day( $boundaries['start'], $boundaries['end'] );

		// Fill date gaps to ensure all days are represented.
		$labels          = [];
		$points_earned   = [];
		$points_redeemed = [];

		for ( $i = $days - 1; $i >= 0; $i-- ) {
			$date              = gmdate( 'Y-m-d', strtotime( "-{$i} days" ) );
			$labels[]          = gmdate( 'M j', strtotime( $date ) );
			$points_earned[]   = $earned_by_day[ $date ] ?? 0;
			$points_redeemed[] = $redeemed_by_day[ $date ] ?? 0;
		}

		// Calculate current period redemption rate (RR).
		$total_earned    = array_sum( $points_earned );
		$total_redeemed  = array_sum( $points_redeemed );
		$redemption_rate = $total_earned > 0 ? round( ( $total_redeemed / $total_earned ) * 100, 1 ) : 0;

		// Calculate previous period RR for delta (pp change).
		$prev_earned   = $this->dashboard_repository->get_points_earned( $boundaries['prev_start'], $boundaries['prev_end'] );
		$prev_redeemed = $this->dashboard_repository->get_points_redeemed( $boundaries['prev_start'], $boundaries['prev_end'] );
		$prev_rr       = $prev_earned > 0 ? round( ( $prev_redeemed / $prev_earned ) * 100, 1 ) : 0;
		$rr_delta      = $redemption_rate - $prev_rr;

		return [
			'labels'          => $labels,
			'datasets'        => [
				[
					'name'  => __( 'Points earned', 'rewardswp' ),
					'type'  => 'line',
					'data'  => $points_earned,
					'color' => '#2563eb',
				],
				[
					'name'  => __( 'Points redeemed', 'rewardswp' ),
					'type'  => 'line',
					'data'  => $points_redeemed,
					'color' => '#fb923c',
				],
			],
			'badge'           => [
				'rr'           => $redemption_rate,
				'rr_formatted' => 'RR: ' . $redemption_rate . '%',
				'delta'        => $rr_delta > 0 ? '+' . number_format( $rr_delta, 1 ) . ' pp' : number_format( $rr_delta, 1 ) . ' pp',
				'delta_type'   => $rr_delta > 0 ? 'positive' : 'negative',
			],
			'tooltip_data'    => [
				'daily_rr' => array_map(
					function ( $earned, $redeemed ) {
						return $earned > 0 ? round( ( $redeemed / $earned ) * 100, 1 ) : 0;
					},
					$points_earned,
					$points_redeemed
				),
			],
			'feature_enabled' => $this->is_feature_enabled( 'points' ),
		];
	}

	/**
	 * Fetch member growth chart data.
	 *
	 * @since 1.0.0
	 * @param string $range The date range.
	 * @return array Chart data for member growth.
	 */
	private function fetch_member_growth_chart_data( string $range ): array {
		$days    = $this->get_date_ranges()[ $range ]['days'] ?? 30;
		$labels  = [];
		$members = [];

		// Generate demo data.
		for ( $i = $days - 1; $i >= 0; $i-- ) {
			$labels[]  = gmdate( 'M j', strtotime( "-{$i} days" ) );
			$members[] = wp_rand( 1, 8 );
		}

		return [
			'labels'   => $labels,
			'datasets' => [
				[
					'name'  => __( 'New members', 'rewardswp' ),
					'data'  => $members,
					'color' => '#10B981',
				],
			],
		];
	}

	/**
	 * Get KPIs for a specific plan.
	 *
	 * @since 1.0.0
	 * @param string $plan The plan level (lite, basic, plus, pro).
	 * @return array Filtered and ordered KPI definitions.
	 */
	public function get_kpis_for_plan( string $plan ): array {
		$all_kpis = $this->get_kpi_definitions();

		// Filter KPIs by plan availability.
		$filtered = array_filter(
			$all_kpis,
			function ( $kpi ) use ( $plan ) {
				return in_array( $plan, $kpi['plans'], true );
			}
		);

		// Sort by priority.
		uasort(
			$filtered,
			function ( $a, $b ) {
				return ( $a['priority'] ?? 99 ) <=> ( $b['priority'] ?? 99 );
			}
		);

		return $filtered;
	}

	/**
	 * Get charts for a specific plan.
	 *
	 * @since 1.0.0
	 * @param string $plan The plan level.
	 * @return array Filtered and ordered chart definitions.
	 */
	public function get_charts_for_plan( string $plan ): array {
		$all_charts = $this->get_chart_definitions();

		// Filter charts by plan availability.
		$filtered = array_filter(
			$all_charts,
			function ( $chart ) use ( $plan ) {
				return in_array( $plan, $chart['plans'], true );
			}
		);

		// Sort by priority.
		uasort(
			$filtered,
			function ( $a, $b ) {
				return ( $a['priority'] ?? 99 ) <=> ( $b['priority'] ?? 99 );
			}
		);

		return $filtered;
	}

	/**
	 * Determine which KPI should be featured.
	 *
	 * @since 1.0.0
	 * @param array $kpi_data The current KPI data.
	 * @return string The featured KPI identifier.
	 */
	public function get_featured_kpi( array $kpi_data ): string {
		return 'rewards-applied-revenue';
	}

	/**
	 * Get date range configuration for a specific plan.
	 *
	 * @since 1.0.0
	 * @param string $plan The plan level.
	 * @return array Date range configuration.
	 */
	public function get_date_range_config( string $plan ): array {
		$configs = [
			'lite'  => [
				'show_picker' => false,
				'default'     => 'last_30_days',
				'comparison'  => false,
			],
			'basic' => [
				'show_picker' => true,
				'default'     => 'last_30_days',
				'ranges'      => [ 'last_7_days', 'last_14_days', 'last_30_days', 'last_60_days', 'last_90_days' ],
				'custom'      => false,
				'comparison'  => true,
			],
			'plus'  => [
				'show_picker' => true,
				'default'     => 'last_30_days',
				'ranges'      => [ 'last_7_days', 'last_14_days', 'last_30_days', 'last_60_days', 'last_90_days' ],
				'custom'      => true,
				'comparison'  => true,
			],
			'pro'   => [
				'show_picker' => true,
				'default'     => 'last_30_days',
				'ranges'      => [ 'last_7_days', 'last_14_days', 'last_30_days', 'last_60_days', 'last_90_days' ],
				'custom'      => true,
				'comparison'  => true,
			],
		];

		return $configs[ $plan ] ?? $configs['lite'];
	}


	/**
	 * Get date boundaries for current and previous periods.
	 *
	 * Returns start/end dates for both current and previous (comparison) periods.
	 *
	 * @since 1.1.0
	 *
	 * @param string $range The date range identifier (e.g., 'last_30_days').
	 *
	 * @return array {
	 *     Date boundaries in Y-m-d H:i:s format.
	 *
	 *     @type string $start      Start date of current period.
	 *     @type string $end        End date of current period.
	 *     @type string $prev_start Start date of previous period.
	 *     @type string $prev_end   End date of previous period.
	 * }
	 */
	protected function get_date_boundaries( string $range ): array {
		$days = $this->get_days_for_range( $range );

		// Use WordPress timezone for date boundaries.
		$timezone = wp_timezone();
		$now      = new \DateTime( 'now', $timezone );

		// Current period: from X days ago to end of today.
		$end_date   = clone $now;
		$start_date = clone $now;
		$start_date->modify( "-{$days} days" );

		$end   = $end_date->format( 'Y-m-d 23:59:59' );
		$start = $start_date->format( 'Y-m-d 00:00:00' );

		// Previous period: same length, ending where current period starts.
		$prev_end_date   = clone $start_date;
		$prev_start_date = clone $start_date;
		$prev_start_date->modify( "-{$days} days" );

		$prev_end   = $prev_end_date->format( 'Y-m-d 23:59:59' );
		$prev_start = $prev_start_date->format( 'Y-m-d 00:00:00' );

		return [
			'start'      => $start,
			'end'        => $end,
			'prev_start' => $prev_start,
			'prev_end'   => $prev_end,
		];
	}

	/**
	 * Get number of days for a given range.
	 *
	 * @since 1.0.0
	 *
	 * @param string $range The date range.
	 *
	 * @return int Number of days.
	 */
	protected function get_days_for_range( string $range ): int {
		$days_map = [
			'last_7_days'  => 7,
			'last_14_days' => 14,
			'last_30_days' => 30,
			'last_60_days' => 60,
			'last_90_days' => 90,
		];

		return $days_map[ $range ] ?? 30;
	}

	/**
	 * Fill date gaps in sparkline data.
	 *
	 * Ensures all days in the period have a value (0 if no data).
	 *
	 * @since 1.1.0
	 *
	 * @param array $daily_data Associative array with date as key and count as value.
	 * @param int   $days       Number of days in the period.
	 *
	 * @return array Indexed array with values for all days.
	 */
	private function fill_date_gaps( array $daily_data, int $days ): array {
		$filled_data = [];

		for ( $i = $days - 1; $i >= 0; $i-- ) {
			$date          = gmdate( 'Y-m-d', strtotime( "-{$i} days" ) );
			$filled_data[] = $daily_data[ $date ] ?? 0;
		}

		return $filled_data;
	}

	/**
	 * Calculate median from an array of numbers.
	 *
	 * @since 1.1.0
	 *
	 * @param array $numbers Array of numeric values.
	 * @return int|null Median value (rounded to nearest integer) or null if empty.
	 */
	protected function calculate_median( array $numbers ): ?int {
		if ( empty( $numbers ) ) {
			return null;
		}

		sort( $numbers );
		$count  = count( $numbers );
		$middle = floor( $count / 2 );

		if ( 0 === ( $count % 2 ) ) {
			// Even count: average of two middle values.
			$median = ( $numbers[ $middle - 1 ] + $numbers[ $middle ] ) / 2;
		} else {
			// Odd count: middle value.
			$median = $numbers[ $middle ];
		}

		return (int) round( $median );
	}

	/**
	 * Calculate rewards-applied revenue KPI with percentage change.
	 *
	 * Returns net revenue from product sales influenced by RewardsWP (orders with
	 * referrals or reward redemptions), minus refunds, with comparison to previous period.
	 *
	 * Formula: Net Revenue = Product Sales After Discounts − Product Refunds
	 *
	 * @since 1.0.0
	 *
	 * @param string $start_date      Current period start date (Y-m-d H:i:s).
	 * @param string $end_date        Current period end date (Y-m-d H:i:s).
	 * @param string $prev_start_date Previous period start date (Y-m-d H:i:s).
	 * @param string $prev_end_date   Previous period end date (Y-m-d H:i:s).
	 *
	 * @return array {
	 *     @type string $value             Formatted net revenue (e.g., "$12,500.50").
	 *     @type float  $value_raw         Raw net revenue value for calculations.
	 *     @type string $change_percentage Percentage change vs previous period (e.g., "+15%").
	 *     @type string $badge             Status badge: 'healthy' (>= 0%), 'warning' (-10% to -1%), 'critical' (< -10%).
	 *     @type string $discounts         Formatted total discounts (for footer).
	 *     @type string $refunds           Formatted total refunds (for footer).
	 *     @type array  $sparkline         Daily net revenue data for chart.
	 * }
	 */
	public function calculate_rewards_applied_revenue(
		string $start_date,
		string $end_date,
		string $prev_start_date,
		string $prev_end_date
	): array {
		// Get current period data.
		$current_data = $this->dashboard_repository->get_rewards_applied_revenue( $start_date, $end_date );

		// Get previous period data.
		$previous_data = $this->dashboard_repository->get_rewards_applied_revenue( $prev_start_date, $prev_end_date );

		$current_revenue  = $current_data['net_revenue'];
		$previous_revenue = $previous_data['net_revenue'];

		// Calculate percentage change.
		$change_percentage = 0.0;
		if ( $previous_revenue > 0 ) {
			$change_percentage = ( ( $current_revenue - $previous_revenue ) / $previous_revenue ) * 100;
		} elseif ( $current_revenue > 0 ) {
			$change_percentage = 100.0; // From 0 to positive is 100% increase.
		}

		// Determine badge status.
		$badge = 'healthy'; // Default for positive growth.
		if ( $change_percentage < -10 ) {
			$badge = 'critical'; // Large drop.
		} elseif ( $change_percentage < 0 ) {
			$badge = 'warning'; // Small drop.
		}

		// Format values for display.
		$formatted_value     = format_currency( $current_revenue );
		$formatted_discounts = format_currency( $current_data['discounts'] );
		$formatted_refunds   = format_currency( $current_data['refunds'] );

		// Format percentage change.
		$formatted_change = ( $change_percentage > 0 ? '+' : '' ) . number_format( $change_percentage, 0 ) . '%';

		// Get daily data for sparkline.
		$daily_data = $this->dashboard_repository->get_rewards_applied_revenue_by_day( $start_date, $end_date );

		// Prepare sparkline data (extract only 'net' values).
		$sparkline = [];
		foreach ( $daily_data as $date => $data ) {
			$sparkline[] = [
				'date'    => $date,
				'value'   => $data['net'] ?? 0.0,
				'refunds' => $data['refunds'] ?? 0.0, // For tooltip.
			];
		}

		return [
			'value'             => $formatted_value,
			'value_raw'         => $current_revenue,
			'change_percentage' => $formatted_change,
			'badge'             => $badge,
			'discounts'         => $formatted_discounts,
			'discounts_raw'     => (float) $current_data['discounts'],
			'refunds'           => $formatted_refunds,
			'refunds_raw'       => (float) $current_data['refunds'],
			'sparkline'         => $sparkline,
		];
	}

	/**
	 * Aggregate sparkline data by granularity for reports charts.
	 *
	 * Converts daily data into weekly or monthly buckets based on the date range:
	 * - ≤ 30 days: daily buckets (no change)
	 * - 31–180 days: weekly buckets
	 * - > 180 days: monthly buckets
	 *
	 * @since 1.1.0
	 *
	 * @param array  $sparkline_data Daily sparkline data with 'x', 'y', 'refunds' keys.
	 * @param string $range          Date range identifier (e.g., 'last_30_days').
	 *
	 * @return array Aggregated data with datetime x values for ApexCharts.
	 */
	public function aggregate_sparkline_for_reports( array $sparkline_data, string $range ): array {
		if ( empty( $sparkline_data ) ) {
			return [];
		}

		$days = $this->get_days_for_range( $range );

		// Determine granularity based on number of days.
		if ( $days <= 30 ) {
			// Daily buckets - include refunds data for tooltip.
			return array_map(
				function ( $point ) {
					return [
						'x'       => strtotime( $point['x'] ?? $point['dateISO'] ?? '' ) * 1000, // Unix timestamp in ms.
						'y'       => (float) ( $point['y'] ?? 0 ),
						'refunds' => (float) ( $point['refunds'] ?? 0 ),
					];
				},
				$sparkline_data
			);
		}

		// Weekly buckets (31-180 days).
		$aggregated = [];
		$timezone   = wp_timezone();

		foreach ( $sparkline_data as $point ) {
			$date_str = $point['x'] ?? $point['dateISO'] ?? '';
			if ( empty( $date_str ) ) {
				continue;
			}

			$date = new \DateTime( $date_str, $timezone );

			// Get the Monday of the week for this date (ISO week starts on Monday).
			$week_start  = clone $date;
			$day_of_week = (int) $date->format( 'N' ); // 1 (Mon) to 7 (Sun).
			$week_start->modify( '-' . ( $day_of_week - 1 ) . ' days' );
			$week_key = $week_start->format( 'Y-m-d' );

			if ( ! isset( $aggregated[ $week_key ] ) ) {
				$aggregated[ $week_key ] = [
					'x'       => strtotime( $week_key ) * 1000, // Unix timestamp in ms.
					'y'       => 0.0,
					'refunds' => 0.0,
				];
			}

			$aggregated[ $week_key ]['y']       += (float) ( $point['y'] ?? 0 );
			$aggregated[ $week_key ]['refunds'] += (float) ( $point['refunds'] ?? 0 );
		}

		// Sort by date and return indexed array.
		ksort( $aggregated );
		return array_values( $aggregated );
	}

	/**
	 * Get the granularity label for a date range.
	 *
	 * @since 1.0.0
	 *
	 * @param string $range Date range identifier.
	 *
	 * @return string Granularity: 'daily', 'weekly', or 'monthly'.
	 */
	public function get_granularity_for_range( string $range ): string {
		$days = $this->get_days_for_range( $range );

		if ( $days <= 30 ) {
			return 'daily';
		}

		if ( $days <= 180 ) {
			return 'weekly';
		}

		return 'monthly';
	}

	/**
	 * Get member growth data for the Reports page chart.
	 *
	 * Returns sparkline data with both new members per day and cumulative total members.
	 * Also calculates delta vs previous period for the subtitle.
	 *
	 * @since 1.1.0
	 *
	 * @param string $range Date range identifier (e.g., 'last_30_days').
	 *
	 * @return array {
	 *     Member growth data.
	 *     @type int    $total_new      New members in this period.
	 *     @type int    $total_members  Total cumulative members at end of period.
	 *     @type string $delta          Formatted delta percentage (e.g., '+12%').
	 *     @type string $delta_class    CSS class for delta display.
	 *     @type array  $sparkline      Daily data with x, new_members, total_members.
	 * }
	 */
	public function get_member_growth_data( string $range ): array {
		$boundaries = $this->get_date_boundaries( $range );

		// Get new members per day in range.
		$daily_new = $this->dashboard_repository->get_new_members_by_day(
			$boundaries['start'],
			$boundaries['end']
		);

		// Get base count (members before start date).
		$base_count = $this->dashboard_repository->get_total_members_before_date(
			$boundaries['start']
		);

		// Build sparkline with cumulative totals.
		$sparkline     = [];
		$running_total = $base_count;
		$timezone      = wp_timezone();

		$period = new \DatePeriod(
			new \DateTime( $boundaries['start'], $timezone ),
			new \DateInterval( 'P1D' ),
			( new \DateTime( $boundaries['end'], $timezone ) )->modify( '+1 day' )
		);

		foreach ( $period as $date ) {
			$date_key       = $date->format( 'Y-m-d' );
			$new_count      = $daily_new[ $date_key ] ?? 0;
			$running_total += $new_count;

			$sparkline[] = [
				'x'             => $date_key,
				'new_members'   => $new_count,
				'total_members' => $running_total,
			];
		}

		// Calculate delta (new members this period vs previous period).
		$current_new = array_sum( $daily_new );
		$prev_new    = $this->dashboard_repository->get_new_members_count(
			$boundaries['prev_start'],
			$boundaries['prev_end']
		);

		$delta_value   = $current_new - $prev_new;
		$delta_percent = $prev_new > 0
			? round( ( $delta_value / $prev_new ) * 100, 0 )
			: ( $current_new > 0 ? 100 : 0 );

		$delta_class = $delta_value >= 0 ? 'text-emerald-600' : 'text-red-600';
		$delta_sign  = $delta_value >= 0 ? '+' : '';

		return [
			'total_new'     => $current_new,
			'total_members' => $running_total,
			'delta'         => $delta_sign . number_format( $delta_percent ) . '%',
			'delta_class'   => $delta_class,
			'sparkline'     => $sparkline,
		];
	}

	/**
	 * Aggregate member growth sparkline data for Reports page charts.
	 *
	 * Handles granularity (daily/weekly/monthly) aggregation similar to
	 * aggregate_sparkline_for_reports() but for member growth data.
	 *
	 * @since 1.1.0
	 *
	 * @param array  $sparkline_data Daily sparkline data with 'x', 'new_members', 'total_members' keys.
	 * @param string $range          Date range identifier (e.g., 'last_30_days').
	 *
	 * @return array Aggregated data with datetime x values for ApexCharts.
	 */
	public function aggregate_member_growth_for_reports( array $sparkline_data, string $range ): array {
		if ( empty( $sparkline_data ) ) {
			return [];
		}

		$days = $this->get_days_for_range( $range );

		// Daily buckets - convert to timestamps.
		if ( $days <= 30 ) {
			return array_map(
				function ( $point ) {
					return [
						'x'             => strtotime( $point['x'] ) * 1000, // Unix timestamp in ms.
						'new_members'   => (int) $point['new_members'],
						'total_members' => (int) $point['total_members'],
					];
				},
				$sparkline_data
			);
		}

		// Weekly buckets (31-180 days).
		$aggregated = [];
		$timezone   = wp_timezone();

		foreach ( $sparkline_data as $point ) {
			$date_str = $point['x'] ?? '';
			if ( empty( $date_str ) ) {
				continue;
			}

			$date = new \DateTime( $date_str, $timezone );

			// Get the Monday of the week for this date (ISO week starts on Monday).
			$week_start  = clone $date;
			$day_of_week = (int) $date->format( 'N' ); // 1 (Mon) to 7 (Sun).
			$week_start->modify( '-' . ( $day_of_week - 1 ) . ' days' );
			$week_key = $week_start->format( 'Y-m-d' );

			if ( ! isset( $aggregated[ $week_key ] ) ) {
				$aggregated[ $week_key ] = [
					'x'             => strtotime( $week_key ) * 1000, // Unix timestamp in ms.
					'new_members'   => 0,
					'total_members' => 0,
				];
			}

			$aggregated[ $week_key ]['new_members'] += (int) $point['new_members'];
			// Take the latest total_members for this week (end of week cumulative).
			$aggregated[ $week_key ]['total_members'] = (int) $point['total_members'];
		}

		// Sort by date and return indexed array.
		ksort( $aggregated );
		return array_values( $aggregated );
	}

	/**
	 * Get orders using rewards time-series data for Reports page.
	 *
	 * Lite stub: Orders using rewards chart is a Pro feature.
	 * Returns empty array for Lite users.
	 *
	 * @since 1.1.0
	 *
	 * @param string $range Date range identifier (e.g., 'last_30_days').
	 *
	 * @return array Always returns empty array in Lite version.
	 */
	public function get_orders_using_rewards_data( string $range ): array {
		// Lite: Orders using rewards chart not available.
		return [];
	}

	/**
	 * Aggregate orders using rewards sparkline data for Reports page charts.
	 *
	 * Lite stub: Orders using rewards chart is a Pro feature.
	 * Returns empty array for Lite users.
	 *
	 * @since 1.1.0
	 *
	 * @param array  $sparkline_data Daily sparkline data with 'x', 'orders_with_rewards', 'total_orders' keys.
	 * @param string $range          Date range identifier (e.g., 'last_30_days').
	 *
	 * @return array Always returns empty array in Lite version.
	 */
	public function aggregate_orders_using_rewards_for_reports( array $sparkline_data, string $range ): array {
		// Lite: Orders using rewards chart not available.
		return [];
	}

	/**
	 * Get top referrers for the dashboard.
	 *
	 * @since 1.1.0
	 *
	 * @param string $range Date range identifier (e.g., 'last_30_days').
	 * @param int    $limit Maximum number of results.
	 *
	 * @return array Array of top referrers with advocate_id, name, and referral_count.
	 */
	public function get_top_referrers( string $range = 'last_30_days', int $limit = 5 ): array {
		$dates   = $this->get_date_boundaries( $range );
		$results = $this->dashboard_repository->get_top_referrers(
			$dates['start'],
			$dates['end'],
			$limit
		);

		// Format results with display name.
		$formatted = array_map(
			function ( $row ) {
				$name = trim( ( $row['first_name'] ?? '' ) . ' ' . ( $row['last_name'] ?? '' ) );
				if ( empty( $name ) ) {
					$name = $row['email'] ?? __( 'Unknown', 'rewardswp' );
				}
				return [
					'advocate_id'    => $row['advocate_id'],
					'name'           => $name,
					'referral_count' => (int) $row['referral_count'],
				];
			},
			$results
		);

		/**
		 * Filters the top referrers data for the dashboard.
		 *
		 * @since 1.1.0
		 *
		 * @param array  $formatted Array of top referrers with advocate_id, name, referral_count.
		 * @param string $range     Date range identifier.
		 * @param int    $limit     Maximum number of results.
		 */
		return apply_filters( 'rewardswp_dashboard_top_referrers', $formatted, $range, $limit );
	}

	/**
	 * Get top redeemed rewards for the dashboard.
	 *
	 * @since 1.1.0
	 *
	 * @param string $range Date range identifier (e.g., 'last_30_days').
	 * @param int    $limit Maximum number of results.
	 *
	 * @return array Array of top rewards with reward_type, amount, free_product_name, redemption_count, and pct_of_top.
	 */
	public function get_top_redeemed_rewards( string $range = 'last_30_days', int $limit = 5 ): array {
		$dates   = $this->get_date_boundaries( $range );
		$results = $this->dashboard_repository->get_top_redeemed_rewards(
			$dates['start'],
			$dates['end'],
			$limit
		);

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

		$max_count = (int) $results[0]['redemption_count'];

		$formatted = array_map(
			function ( $row ) use ( $max_count ) {
				return [
					'reward_type'       => $row['reward_type'],
					'amount'            => $row['amount'],
					'free_product_name' => $row['free_product_name'] ?? '',
					'redemption_count'  => (int) $row['redemption_count'],
					'pct_of_top'        => $max_count > 0 ? round( ( $row['redemption_count'] / $max_count ) * 100 ) : 0,
				];
			},
			$results
		);

		/**
		 * Filters the top redeemed rewards data for the dashboard.
		 *
		 * @since 1.1.0
		 *
		 * @param array  $formatted Array of top rewards with reward_type, amount, free_product_name, redemption_count, pct_of_top.
		 * @param string $range     Date range identifier.
		 * @param int    $limit     Maximum number of results.
		 */
		return apply_filters( 'rewardswp_dashboard_top_redeemed_rewards', $formatted, $range, $limit );
	}

	/**
	 * Get recent activity for the dashboard activity feed.
	 *
	 * Fetches recent events and transforms them into view-ready format with
	 * appropriate colors, descriptions, and relative timestamps.
	 *
	 * @since 1.1.0
	 *
	 * @param int $limit Maximum number of activities to return. Default 7.
	 *
	 * @return array Array of formatted activity items ready for rendering.
	 */
	public function get_recent_activity( int $limit = 7 ): array {
		$raw_activities = $this->dashboard_repository->get_recent_activity( $limit );

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

		$formatted = array_map( [ $this, 'transform_activity' ], $raw_activities );

		/**
		 * Filters the recent activity data for the dashboard.
		 *
		 * @since 1.1.0
		 *
		 * @param array $formatted Array of activity items with type, dot_color, user, description, time.
		 * @param int   $limit     Maximum number of activities.
		 */
		return apply_filters( 'rewardswp_dashboard_recent_activity', $formatted, $limit );
	}

	/**
	 * Transform a raw activity row into view-ready format.
	 *
	 * @since 1.1.0
	 *
	 * @param array $activity Raw activity data from database.
	 *
	 * @return array Transformed activity with type, dot_color, user, user_url, description, meta, time.
	 */
	private function transform_activity( array $activity ): array {
		$activity_type = $activity['activity_type'] ?? 'unknown';

		// Build user display name (fallback to email if no name).
		$first_name = trim( $activity['first_name'] ?? '' );
		$last_name  = trim( $activity['last_name'] ?? '' );
		$user_name  = trim( $first_name . ' ' . $last_name );

		if ( empty( $user_name ) ) {
			$email = $activity['email'] ?? '';
			// Show just the local part of email (before @) for privacy.
			if ( ! empty( $email ) && str_contains( $email, '@' ) ) {
				$user_name = strstr( $email, '@', true );
			} else {
				$user_name = __( 'A member', 'rewardswp' );
			}
		}

		// Build member profile URL.
		$member_id = $activity['member_id'] ?? 0;
		$user_url  = $member_id > 0
			? admin_url( 'admin.php?page=rewardswp-members&action=view_member&member_id=' . $member_id )
			: '#';

		// Get activity configuration.
		$config = $this->get_activity_config( $activity_type, $activity );

		$result = [
			'type'        => $activity_type,
			'dot_color'   => $config['dot_color'],
			'user'        => $user_name,
			'user_url'    => $user_url,
			'description' => $config['description'],
			'meta'        => $config['meta'],
			'time'        => $this->format_relative_time( $activity['event_time'] ?? '' ),
		];

		// Add order link for order events.
		if ( ! empty( $config['order_id'] ) && ! empty( $config['order_url'] ) ) {
			$result['order_id']  = $config['order_id'];
			$result['order_url'] = $config['order_url'];
		}

		return $result;
	}

	/**
	 * Get activity type configuration including color and description.
	 *
	 * @since 1.1.0
	 *
	 * @param string $activity_type The type of activity.
	 * @param array  $activity      The full activity data.
	 *
	 * @return array Configuration with dot_color, description, meta, and optional order_id/order_url.
	 */
	private function get_activity_config( string $activity_type, array $activity ): array {
		$amount        = isset( $activity['amount'] ) ? absint( $activity['amount'] ) : 0;
		$source_type   = $activity['source_type'] ?? '';
		$source_id     = isset( $activity['source_id'] ) ? absint( $activity['source_id'] ) : 0;
		$meta_value    = $activity['meta'] ?? '';
		$reward_type   = $activity['reward_type'] ?? '';
		$reward_amount = isset( $activity['reward_amount'] ) ? (float) $activity['reward_amount'] : 0.0;

		switch ( $activity_type ) {
			case 'order_placed':
				$order_url = '';
				if ( $source_id > 0 ) {
					// Try primary integration first.
					$order_url = $this->integration->get_order_link( $source_id );

					// If empty, try other integrations (handles multi-integration setups).
					if ( empty( $order_url ) ) {
						$integrations_to_try = [
							\Awesomemotive\Rewardswp\Integrations\WooCommerceIntegration::class,
							\Awesomemotive\Rewardswp\Integrations\EasyDigitalDownloadsIntegration::class,
						];
						foreach ( $integrations_to_try as $integration_class ) {
							$alt_integration = $this->integrations_factory->get_integration( $integration_class );
							if ( $alt_integration && $alt_integration->is_active() ) {
								$order_url = $alt_integration->get_order_link( $source_id );
								if ( ! empty( $order_url ) ) {
									break;
								}
							}
						}
					}
				}

				// Check if this is a first order bonus.
				if ( 'first_order' === $source_type ) {
					$description = sprintf(
						/* translators: %s: number of points */
						__( 'earned %s points as a first order bonus for', 'rewardswp' ),
						number_format_i18n( $amount )
					);
				} else {
					$description = sprintf(
						/* translators: %s: number of points */
						__( 'earned %s points for', 'rewardswp' ),
						number_format_i18n( $amount )
					);
				}

				return [
					'dot_color'   => 'bg-green-500',
					'description' => $description,
					'meta'        => '',
					'order_id'    => $source_id,
					'order_url'   => $order_url,
				];

			case 'points_earned':
				$action = $this->get_points_action_description( $source_type );
				return [
					'dot_color'   => 'bg-green-500',
					'description' => sprintf(
						/* translators: 1: number of points, 2: action description */
						__( 'earned %1$s points %2$s', 'rewardswp' ),
						number_format_i18n( $amount ),
						$action
					),
					'meta'        => '',
				];

			case 'reward_redeemed':
				// Format reward name from reward_type and reward_amount if available.
				$reward_label     = '';
				$free_product_name = $activity['free_product_name'] ?? '';

				if ( ! empty( $reward_type ) ) {
					// For free products, use the actual product name if available.
					if ( 'free_product' === $reward_type && ! empty( $free_product_name ) ) {
						$reward_label = sprintf(
							/* translators: %s: product name */
							__( 'Free %s', 'rewardswp' ),
							$free_product_name
						);
					} else {
						$reward_label = get_reward_display_title_from_values( null, $reward_type, $reward_amount );
					}
				}

				// Build description with reward name if available.
				if ( ! empty( $reward_label ) ) {
					$description = sprintf(
						/* translators: %s: reward name (e.g., "$5 off") */
						__( 'redeemed %s', 'rewardswp' ),
						esc_html( $reward_label )
					);
				} else {
					$description = __( 'redeemed a reward', 'rewardswp' );
				}

				// Get order link if available (referral rewards have order_id via referral).
				$order_url = $source_id > 0 ? $this->integration->get_order_link( $source_id ) : '';

				return [
					'dot_color'   => 'bg-blue-500',
					'description' => $description,
					'meta'        => $amount > 0
						? sprintf(
							/* translators: %s: number of points */
							__( '%s points', 'rewardswp' ),
							number_format_i18n( $amount )
						)
						: '',
					'order_id'    => $source_id,
					'order_url'   => $order_url,
				];

			case 'referral_completed':
				$referred_name = ! empty( $meta_value ) ? $meta_value : __( 'someone', 'rewardswp' );
				$order_url     = $source_id > 0 ? $this->integration->get_order_link( $source_id ) : '';
				return [
					'dot_color'   => 'bg-purple-500',
					'description' => sprintf(
						/* translators: %s: name of referred person */
						__( 'successfully referred %s', 'rewardswp' ),
						esc_html( $referred_name )
					),
					'meta'        => '',
					'order_id'    => $source_id,
					'order_url'   => $order_url,
				];

			case 'member_joined':
				return [
					'dot_color'   => 'bg-green-500',
					'description' => __( 'joined the rewards program', 'rewardswp' ),
					'meta'        => '',
				];

			default:
				return [
					'dot_color'   => 'bg-gray-500',
					'description' => __( 'performed an action', 'rewardswp' ),
					'meta'        => '',
				];
		}
	}

	/**
	 * Get human-readable description for points earning actions.
	 *
	 * @since 1.1.0
	 *
	 * @param string $source_type The source type from the points record.
	 *
	 * @return string Action description.
	 */
	private function get_points_action_description( string $source_type ): string {
		switch ( $source_type ) {
			case 'product_review':
			case 'edd_product_review':
				return __( 'for writing a product review', 'rewardswp' );

			case 'signup':
				return __( 'for joining the rewards program', 'rewardswp' );

			case 'referral':
			case 'referral_reward':
				return __( 'from referral bonus', 'rewardswp' );

			case 'birthday':
				return __( 'for their birthday', 'rewardswp' );

			default:
				return '';
		}
	}

	/**
	 * Format a timestamp as a human-readable relative time.
	 *
	 * @since 1.1.0
	 *
	 * @param string $datetime The datetime string (in site's local timezone).
	 *
	 * @return string Formatted relative time.
	 */
	private function format_relative_time( string $datetime ): string {
		return get_human_readable_time_ago( $datetime );
	}

	/**
	 * Get the applicable empty state configuration for a KPI.
	 *
	 * Evaluates conditions to determine which empty state case applies,
	 * or returns the simple empty_state if no cases are defined.
	 *
	 * @since 1.1.0
	 *
	 * @param string $kpi_id   The KPI identifier.
	 * @param array  $kpi_data The KPI data (values, feature_enabled, etc.).
	 *
	 * @return array|null The applicable empty state config, or null if none.
	 */
	public function get_empty_state_for_kpi( string $kpi_id, array $kpi_data ): ?array {
		$definitions = $this->get_kpi_definitions();
		$kpi_def     = $definitions[ $kpi_id ] ?? null;

		if ( empty( $kpi_def['empty_state'] ) ) {
			return null;
		}

		$empty_state = $kpi_def['empty_state'];

		// If has cases, evaluate them in order.
		if ( ! empty( $empty_state['cases'] ) ) {
			foreach ( $empty_state['cases'] as $case ) {
				if ( $this->evaluate_empty_state_condition( $case['condition'] ?? '', $kpi_data ) ) {
					return $case;
				}
			}
			return null;
		}

		// For simple empty_state (no cases), check condition if present.
		if ( ! empty( $empty_state['condition'] ) ) {
			if ( $this->evaluate_empty_state_condition( $empty_state['condition'], $kpi_data ) ) {
				return $empty_state;
			}
			return null;
		}

		// No condition specified - don't show empty state.
		return null;
	}

	/**
	 * Get program status for a KPI when feature is disabled but data exists.
	 *
	 * Returns badge info to display when the program is off but historical data
	 * should still be shown. This allows showing the actual KPI value with a
	 * "Program off" indicator rather than hiding the data.
	 *
	 * @since 1.1.0
	 *
	 * @param string $kpi_id   The KPI identifier.
	 * @param array  $kpi_data The KPI data (values, feature_enabled, etc.).
	 *
	 * @return array|null Badge info array with 'badge' and 'message' keys, or null.
	 */
	public function get_program_status_for_kpi( string $kpi_id, array $kpi_data ): ?array {
		$definitions = $this->get_kpi_definitions();
		$kpi_def     = $definitions[ $kpi_id ] ?? null;

		// Only applies to KPIs with program_status config.
		if ( empty( $kpi_def['program_status'] ) ) {
			return null;
		}

		$feature_enabled = $kpi_data['feature_enabled'] ?? true;

		// Determine if data exists based on KPI type.
		$has_data = false;
		switch ( $kpi_id ) {
			case 'points-activity':
				// Check actual numeric fields for points.
				$has_data = ! empty( $kpi_data['points_earned'] ) || ! empty( $kpi_data['points_redeemed'] );
				break;
			case 'completed-referrals':
				// Check numeric value for referrals.
				$has_data = ! empty( $kpi_data['value'] ) && is_numeric( $kpi_data['value'] ) && (int) $kpi_data['value'] > 0;
				break;
			default:
				// Fallback: check value or qualified flag.
				$has_data = ! empty( $kpi_data['value'] ) || ! empty( $kpi_data['qualified'] );
				break;
		}

		// Return status badge only if program is off AND data exists.
		if ( ! $feature_enabled && $has_data ) {
			return [
				'badge'   => $kpi_def['program_status']['disabled_badge'] ?? __( 'Program off', 'rewardswp' ),
				'message' => $kpi_def['program_status']['disabled_message'] ?? '',
				'action'  => $kpi_def['program_status']['action'] ?? null,
			];
		}

		return null;
	}

	/**
	 * Evaluate an empty state condition against KPI data.
	 *
	 * Supports simple condition expressions like:
	 * - 'total_members === 0'
	 * - 'value === 0'
	 * - '!feature_enabled'
	 * - 'points_earned === 0 && points_redeemed === 0'
	 *
	 * @since 1.1.0
	 *
	 * @param string $condition The condition expression.
	 * @param array  $kpi_data  The KPI data to evaluate against.
	 *
	 * @return bool Whether the condition is met.
	 */
	private function evaluate_empty_state_condition( string $condition, array $kpi_data ): bool {
		if ( empty( $condition ) ) {
			return false;
		}

		// Handle compound conditions with &&.
		if ( str_contains( $condition, '&&' ) ) {
			$parts = array_map( 'trim', explode( '&&', $condition ) );
			foreach ( $parts as $part ) {
				if ( ! $this->evaluate_empty_state_condition( $part, $kpi_data ) ) {
					return false;
				}
			}
			return true;
		}

		// Handle negation.
		if ( str_starts_with( $condition, '!' ) ) {
			$key = substr( $condition, 1 );
			return empty( $kpi_data[ $key ] );
		}

		// Handle equality checks (=== 0).
		if ( str_contains( $condition, '===' ) ) {
			[ $key, $expected ] = array_map( 'trim', explode( '===', $condition ) );
			$actual             = $kpi_data[ $key ] ?? null;

			// For zero comparisons, treat null/empty as equivalent to 0 (no data scenario).
			if ( '0' === $expected ) {
				return empty( $actual ) || 0 === (int) $actual;
			}

			// phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual
			return $actual == $expected; // Allow loose comparison for type coercion.
		}

		// Simple truthy check.
		return ! empty( $kpi_data[ $condition ] );
	}
}
