<?php
/**
 * REST API controller for Abandoned Cart Recovery Analytics.
 *
 * @package EDD\Pro\CartRecovery\REST\Controllers
 * @copyright Copyright (c) 2026, Sandhills Development, LLC
 * @license https://opensource.org/licenses/GPL-3.0 GNU General Public License v3.0
 * @since 3.6.5
 */

namespace EDD\Pro\CartRecovery\REST\Controllers;

// Exit if accessed directly.
defined( 'ABSPATH' ) || exit;

use EDD\Pro\CartRecovery\Carts\Repository;
use EDD\Pro\CartRecovery\Carts\Cart;
use EDD\Pro\CartRecovery\Traits\Status;
use EDD\Reports\Utilities\Database as ReportsDatabase;

/**
 * Analytics controller class.
 *
 * @since 3.6.5
 */
final class Analytics {
	use Status;

	/**
	 * Permission callback.
	 *
	 * @since 3.6.5
	 * @return bool True if the user can view stats, false otherwise.
	 */
	public function can_view_stats(): bool {
		return current_user_can( 'manage_shop_settings' );
	}

	/**
	 * Stats endpoint callback.
	 *
	 * @since 3.6.5
	 *
	 * @param \WP_REST_Request $request Request object.
	 * @return \WP_REST_Response
	 *     Response object containing stats data.
	 */
	public function get_stats( \WP_REST_Request $request ): \WP_REST_Response {
		// Guard: if feature disabled, avoid heavy queries.
		if ( ! edd_get_option( 'acr_enabled', false ) ) {
			return new \WP_REST_Response( array( 'error' => 'acr_disabled' ), 403 );
		}

		$parsed = $this->parse_dates( $request );

		// Use the Database utility for UTC conversion and period detection.
		$date_range    = ReportsDatabase::parse_date_range_for_query( $parsed['start'], $parsed['end'] );
		$start_utc_str = $date_range['start'];
		$end_utc_str   = $date_range['end'];

		// Display range in store timezone.
		$start_date_str = $parsed['start']->format( 'Y-m-d' );
		$end_date_str   = $parsed['end']->format( 'Y-m-d' );
		$timezone_name  = edd_get_timezone_id();

		global $wpdb;

		$recovered_count = (int) $wpdb->get_var(
			$wpdb->prepare(
				"SELECT COUNT(*) FROM {$wpdb->prefix}edd_acr_carts WHERE status = 'recovered' AND date_recovered BETWEEN %s AND %s",
				$start_utc_str,
				$end_utc_str
			)
		);

		$converted_count = (int) $wpdb->get_var(
			$wpdb->prepare(
				"SELECT COUNT(*) FROM {$wpdb->prefix}edd_acr_carts WHERE status = 'converted' AND date_recovered BETWEEN %s AND %s",
				$start_utc_str,
				$end_utc_str
			)
		);

		$abandoned_count = (int) $wpdb->get_var(
			$wpdb->prepare(
				"SELECT COUNT(*) FROM {$wpdb->prefix}edd_acr_carts WHERE status = 'abandoned' AND date_abandoned BETWEEN %s AND %s",
				$start_utc_str,
				$end_utc_str
			)
		);

		$total_opportunities = max( 0, $recovered_count + $abandoned_count );
		$recovery_rate       = $total_opportunities > 0 ? round( ( $recovered_count / $total_opportunities ) * 100, 2 ) : 0.0;

		// Revenue (orders as source of truth).
		$complete_statuses = (array) edd_get_complete_order_statuses();
		$complete_statuses = array_map( 'strval', $complete_statuses );
		$complete_in       = implode( ',', array_fill( 0, count( $complete_statuses ), '%s' ) );

		$recovered_revenue = (float) $wpdb->get_var(
			$wpdb->prepare(
				"SELECT COALESCE(SUM(o.total),0) FROM {$wpdb->prefix}edd_orders o INNER JOIN {$wpdb->prefix}edd_acr_carts acr ON acr.order_id = o.id
				 WHERE acr.status IN ('recovered', 'converted') AND o.status IN ( {$complete_in} ) AND o.date_created BETWEEN %s AND %s",
				array_merge( $complete_statuses, array( $start_utc_str, $end_utc_str ) )
			)
		);

		// Trends (start/end in store TZ; Repository handles UTC conversion internally).
		$repository    = new Repository();
		$revenue_trend = $this->get_revenue_trend( $parsed['start'], $parsed['end'] );
		$carts_trend   = $repository->get_recovered_trend( $parsed['start'], $parsed['end'] );

		// Top products: Query API compares raw UTC column, so pass UTC bounds.
		$top = $this->compute_top_products( $start_utc_str, $end_utc_str );

		return new \WP_REST_Response(
			array(
				'range'             => array(
					'start'    => $start_date_str,
					'end'      => $end_date_str,
					'timezone' => $timezone_name,
				),
				'currency'          => edd_get_currency(),
				'recovered_revenue' => edd_format_amount( $recovered_revenue, true, '', 'data' ),
				'recovered_count'   => $recovered_count,
				'converted_count'   => $converted_count,
				'abandoned_count'   => $abandoned_count,
				'recovery_rate'     => $recovery_rate,
				'top_abandoned'     => $top['abandoned'],
				'top_recovered'     => $top['recovered'],
				'revenue_trend'     => $revenue_trend,
				'carts_trend'       => $carts_trend,
			),
			200
		);
	}

	/**
	 * Build top products for abandoned and recovered carts.
	 *
	 * @since 3.6.5
	 * @param string $start_mysql Start date in MySQL format.
	 * @param string $end_mysql End date in MySQL format.
	 * @return array
	 *     Array containing top products for abandoned and recovered carts.
	 *     @type array $abandoned Top abandoned products.
	 *     @type array $recovered Top recovered products.
	 */
	private function compute_top_products( string $start_mysql, string $end_mysql ): array {

		// Pull carts via Query API to reuse object shaping.
		$cart_query = new \EDD\Pro\CartRecovery\Database\Queries\RecoveryCart();

		$recovered_carts = (array) $cart_query->query(
			array(
				'status__in' => array( 'recovered', 'converted' ),
				'number'     => -1,
				'date_query' => array(
					array(
						'column' => 'date_recovered',
						'after'  => $start_mysql,
						'before' => $end_mysql,
					),
				),
			)
		);

		$abandoned_carts = (array) $cart_query->query(
			array(
				'status'     => 'abandoned',
				'number'     => 99999,
				'date_query' => array(
					array(
						'column' => 'date_abandoned',
						'after'  => $start_mysql,
						'before' => $end_mysql,
					),
				),
			)
		);

		$product_counts = array(
			'abandoned' => array(),
			'recovered' => array(),
		);

		foreach ( $recovered_carts as $cart ) {
			$this->tally_cart_items( $cart, $product_counts['recovered'] );
		}
		foreach ( $abandoned_carts as $cart ) {
			$this->tally_cart_items( $cart, $product_counts['abandoned'] );
		}

		arsort( $product_counts['abandoned'] );
		arsort( $product_counts['recovered'] );

		$top_abandoned = array_slice( $product_counts['abandoned'], 0, 5, true );
		$top_recovered = array_slice( $product_counts['recovered'], 0, 5, true );

		return array(
			'abandoned' => $this->format_product_counts( $top_abandoned ),
			'recovered' => $this->format_product_counts( $top_recovered ),
		);
	}

	/**
	 * Extract item IDs from cart row and tally counts.
	 *
	 * @since 3.6.5
	 * @param object $cart Cart object.
	 * @param array  $bucket Array containing product counts.
	 * @return void
	 */
	private function tally_cart_items( $cart, array &$bucket ): void {
		if ( ! $cart instanceof Cart ) {
			return;
		}

		foreach ( $cart->get_items() as $item ) {
			$bucket[ $item['id'] ] = ( isset( $bucket[ $item['id'] ] ) ? $bucket[ $item['id'] ] : 0 ) + $item['quantity'];
		}
	}

	/**
	 * Returns recovered revenue trend grouped by day based on orders as source of truth.
	 * Database dates are UTC; start/end are in store timezone and converted to UTC for filtering.
	 * Grouping uses timezone conversion so returned dates are in store timezone.
	 *
	 * @since 3.6.5
	 * @param \DateTimeInterface $start Inclusive start date in site TZ.
	 * @param \DateTimeInterface $end   Inclusive end date in site TZ.
	 * @return array Array of [ [ 'date' => 'YYYY-MM-DD', 'amount' => float ] ]
	 */
	private function get_revenue_trend( \DateTimeInterface $start, \DateTimeInterface $end ): array {
		global $wpdb;
		$date_range = ReportsDatabase::parse_date_range_for_query( $start, $end );
		$clauses    = ReportsDatabase::get_sql_clauses( 'day', 'o.date_created' );

		$orders_table      = isset( $wpdb->edd_orders ) ? $wpdb->edd_orders : $wpdb->prefix . 'edd_orders';
		$complete_statuses = array_map( 'strval', (array) edd_get_complete_order_statuses() );
		$status_in         = $this->get_placeholders( $complete_statuses, 'string' );

		$sql = "SELECT {$clauses['select']}, COALESCE(SUM(o.total),0) AS amount
                FROM {$orders_table} o
                INNER JOIN {$wpdb->prefix}edd_acr_carts acr ON acr.order_id = o.id
                WHERE acr.status IN ('recovered', 'converted') AND o.status IN ( {$status_in} )
                  AND o.date_created BETWEEN %s AND %s
                GROUP BY {$clauses['groupby']}
                ORDER BY {$clauses['orderby']} ASC";

		$prepared = $wpdb->prepare( $sql, array_merge( $complete_statuses, array( $date_range['start'], $date_range['end'] ) ) );
		if ( false === $prepared ) {
			return array();
		}

		$rows = $wpdb->get_results( $prepared );
		$out  = array();
		foreach ( (array) $rows as $row ) {
			$out[] = array(
				'date'   => (string) $row->date,
				'amount' => round( (float) $row->amount, 2 ),
			);
		}

		return $out;
	}

	/**
	 * Format product counts into response objects with titles.
	 *
	 * @since 3.6.5
	 * @param array $counts Array containing product counts.
	 * @return array
	 *     Array containing formatted product counts.
	 *     @type array $product_id Product ID.
	 *     @type array $product_name Product name.
	 *     @type array $count Count.
	 */
	private function format_product_counts( array $counts ): array {
		$result = array();
		foreach ( $counts as $id => $count ) {
			$result[] = array(
				'product_id'   => (int) $id,
				'product_name' => get_the_title( (int) $id ),
				'count'        => (int) $count,
			);
		}
		return $result;
	}

	/**
	 * Parse incoming dates in store timezone using EDD date utilities; default to Last 30 Days.
	 *
	 * @since 3.6.5
	 * @param \WP_REST_Request $request Request object.
	 * @return array
	 *     Array containing start and end as EDD\Utils\Date in store timezone.
	 *     @type \EDD\Utils\Date $start Start date (start of day).
	 *     @type \EDD\Utils\Date $end   End date (end of day).
	 */
	private function parse_dates( \WP_REST_Request $request ): array {
		$start_raw = (string) $request->get_param( 'start_date' );
		$end_raw   = (string) $request->get_param( 'end_date' );
		$tz_id     = edd_get_timezone_id();

		try {
			$end = ! empty( $end_raw )
				? EDD()->utils->date( $end_raw, $tz_id, false )
				: EDD()->utils->date( 'now', $tz_id, false );
		} catch ( \Exception $e ) {
			$end = EDD()->utils->date( 'now', $tz_id, false );
		}

		try {
			if ( ! empty( $start_raw ) ) {
				$start = EDD()->utils->date( $start_raw, $tz_id, false );
			} else {
				$start = $end->copy()->subDays( 30 );
			}
		} catch ( \Exception $e ) {
			$start = $end->copy()->subDays( 30 );
		}

		// Normalize order if reversed.
		if ( $start->gt( $end ) ) {
			$tmp   = $start;
			$start = $end;
			$end   = $tmp;
		}

		return array(
			'start' => $start->copy()->startOfDay(),
			'end'   => $end->copy()->endOfDay(),
		);
	}

	/**
	 * Returns placeholders for wpdb operations.
	 *
	 * @since 3.6.5
	 * @param array  $values Values being persisted.
	 * @param string $type   Type of placeholder to return.
	 * @return string
	 */
	private function get_placeholders( array $values, $type = 'string' ): string {
		if ( 'int' === $type ) {
			return implode( ',', array_fill( 0, count( $values ), '%d' ) );
		}

		return implode( ',', array_fill( 0, count( $values ), '%s' ) );
	}
}
