<?php
/**
 * File to tracking WooCommerce functionality.
 */

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

/**
 * Class to handle WooCommerce tracking.
 */
class SimpleStats_Tracking_WooCommerce extends SimpleStats_Ecommerce_Tracking {

	/**
	 * Add hooks to the method.
	 */
	protected function hooks() {
		// Add Order hooks.
		foreach ( $this->track_order_statuses() as $status ) {
			add_action( 'woocommerce_order_status_' . $status, array( $this, 'add_order' ), 10, 2 );
		}

		// Refund hook.
		add_action( 'woocommerce_order_refunded', array( $this, 'add_refund' ), 10, 2 );

		// Capture the visitor session onto the order at checkout (while cookies
		// exist) so attribution survives a later admin-side status change.
		add_action( 'woocommerce_checkout_create_order', array( $this, 'store_session' ), 10, 1 );                // Classic checkout.
		add_action( 'woocommerce_store_api_checkout_update_order_meta', array( $this, 'store_session' ), 10, 1 ); // Blocks / Store API checkout.

		// Add-to-cart event (fires on classic and AJAX adds).
		add_action( 'woocommerce_add_to_cart', array( $this, 'add_to_cart' ), 10, 6 );

		// Reached-checkout event. template_redirect + is_checkout() covers both
		// classic and Blocks checkout (both render on the checkout page).
		add_action( 'template_redirect', array( $this, 'reached_checkout' ) );
	}

	/**
	 * Purchase order callback.
	 *
	 * @param $order_id int
	 * @param $order    WC_Order
	 */
	public function add_order( $order_id, $order ) {
		$skip_renewals = apply_filters( 'simplestats_ecommerce_skip_renewals', true );
		if ( function_exists( 'wcs_order_contains_subscription' ) && wcs_order_contains_subscription( $order_id, 'renewal' ) && $skip_renewals ) {
			return;
		}

		$meta = $order->get_meta( '_simplestats_is_in_app', true, 'edit' );

		if ( 'yes' === $meta ) {
			return;
		}

		$this->send_request(
			array(
				'type'        => 'transaction',
				'transaction' => $this->build_transaction( $order ),
				'session'     => $this->session_from_order( $order ),
			)
		);

		$order->add_meta_data( '_simplestats_is_in_app', 'yes' );
		$order->save_meta_data();
	}

	/**
	 * Refund callback.
	 *
	 * @param int $order_id  Parent order id.
	 * @param int $refund_id Refund id.
	 */
	public function add_refund( $order_id, $refund_id ) {
		$order  = wc_get_order( $order_id );
		$refund = wc_get_order( $refund_id );

		if ( ! $order || ! $refund ) {
			return;
		}

		$transaction = array(
			'external_id'        => (string) $refund_id,
			'parent_external_id' => (string) $order_id,
			'platform'           => 'woocommerce',
			'kind'               => 'refund',
			'status'             => 'refunded',
			'currency'           => $order->get_currency(),
			'value'              => (float) abs( $refund->get_total() ),
			'customer_hash'      => $this->hash_email( $order->get_billing_email() ),
			'occurred_at'        => gmdate( 'c' ),
			'items'              => $this->build_items( $refund, true ),
		);

		$this->send_request(
			array(
				'type'        => 'transaction',
				'transaction' => $transaction,
				'session'     => $this->session_from_order( $order ),
			)
		);
	}

	/**
	 * Add-to-cart callback. Sends a non-blocking cart_event to the app.
	 *
	 * @param string $cart_item_key  Cart item key.
	 * @param int    $product_id     Product id.
	 * @param int    $quantity       Quantity added.
	 * @param int    $variation_id   Variation id (0 if not a variation).
	 * @param array  $variation      Variation attributes.
	 * @param array  $cart_item_data Extra cart item data.
	 */
	public function add_to_cart( $cart_item_key, $product_id, $quantity, $variation_id = 0, $variation = array(), $cart_item_data = array() ) {
		$product = wc_get_product( $variation_id ? $variation_id : $product_id );
		if ( ! $product ) {
			return;
		}

		list( $external_item_id, $name, $variant ) = $this->product_identity( $product );

		$this->send_request(
			array(
				'type' => 'cart_event',
				'cart' => array(
					'platform'         => 'woocommerce',
					'external_item_id' => $external_item_id,
					'name'             => $name,
					'variant'          => $variant,
					'quantity'         => (int) $quantity,
					'occurred_at'      => gmdate( 'c' ),
				),
			)
		);
	}

	/**
	 * Reached-checkout callback. Fires on the checkout page (classic and Blocks
	 * both render it) and sends a non-blocking begin_checkout event for the
	 * funnel. Skipped on the order-received page and for an empty cart.
	 */
	public function reached_checkout() {
		if ( ! function_exists( 'is_checkout' ) || ! is_checkout() || is_order_received_page() ) {
			return;
		}

		if ( ! WC()->cart || WC()->cart->is_empty() ) {
			return;
		}

		$this->send_request(
			array(
				'type'     => 'begin_checkout',
				'checkout' => array(
					'platform'    => 'woocommerce',
					'occurred_at' => gmdate( 'c' ),
				),
			)
		);
	}

	/**
	 * Capture the visitor session UUIDs onto the order at checkout time.
	 *
	 * Fires while the customer is on-site (cookies present), so the order keeps
	 * its attribution even when the transaction is later sent from wp-admin.
	 *
	 * @param WC_Order $order Order being created.
	 */
	public function store_session( $order ) {
		$session = $this->read_session_cookies();
		if ( empty( $session ) || ! is_a( $order, 'WC_Order' ) ) {
			return;
		}

		$order->update_meta_data( self::META_VISITOR_UUID, $session['visitor_uuid'] );
		$order->update_meta_data( self::META_SESSION_UUID, $session['session_uuid'] );
		if ( ! empty( $session['event_uuid'] ) ) {
			$order->update_meta_data( self::META_EVENT_UUID, $session['event_uuid'] );
		}
		// Persisted by the checkout flow's order save.
	}

	/**
	 * Read the session UUIDs previously captured on an order.
	 *
	 * @param WC_Order $order Order (or parent order, for refunds).
	 * @return array Empty when no session was captured.
	 */
	private function session_from_order( $order ) {
		$visitor = $order->get_meta( self::META_VISITOR_UUID, true, 'edit' );
		if ( empty( $visitor ) ) {
			return array();
		}

		return array(
			'visitor_uuid' => $visitor,
			'session_uuid' => $order->get_meta( self::META_SESSION_UUID, true, 'edit' ),
			'event_uuid'   => $order->get_meta( self::META_EVENT_UUID, true, 'edit' ),
		);
	}

	/**
	 * Build the normalized purchase payload from a WooCommerce order.
	 *
	 * @param WC_Order $order Order object.
	 * @return array
	 */
	private function build_transaction( $order ) {
		$coupons = $order->get_coupon_codes();

		return array(
			'external_id'    => (string) $order->get_id(),
			'platform'       => 'woocommerce',
			'kind'           => 'purchase',
			'status'         => 'completed',
			'currency'       => $order->get_currency(),
			'value'          => (float) $order->get_total( 'edit' ),
			'subtotal'       => (float) $order->get_subtotal(),
			'tax'            => (float) $order->get_total_tax( 'edit' ),
			'shipping'       => (float) $order->get_shipping_total( 'edit' ),
			'discount_total' => (float) $order->get_discount_total( 'edit' ),
			'coupon'         => ! empty( $coupons ) ? implode( ',', $coupons ) : null,
			'customer_hash'  => $this->hash_email( $order->get_billing_email() ),
			'occurred_at'    => $order->get_date_created() ? $order->get_date_created()->format( 'c' ) : gmdate( 'c' ),
			'items'          => $this->build_items( $order, false ),
		);
	}

	/**
	 * Build line items from a WooCommerce order or refund.
	 *
	 * @param WC_Order|WC_Order_Refund $source    Order or refund.
	 * @param bool                     $is_refund Whether quantities/totals are negative.
	 * @return array
	 */
	private function build_items( $source, $is_refund ) {
		$items    = array();
		$position = 0;

		foreach ( $source->get_items() as $line ) {
			$product = $line->get_product();
			list( $external_item_id, $name, $variant ) = $this->product_identity( $product );
			if ( '' === $name ) {
				$name = $line->get_name();
			}

			$quantity      = (int) abs( $line->get_quantity() );
			$line_subtotal = (float) abs( $line->get_subtotal() );
			$line_total    = (float) abs( $line->get_total() );
			$unit_price    = $quantity > 0 ? $line_subtotal / $quantity : $line_subtotal;
			$discount      = $is_refund ? 0.0 : ( $line_subtotal - $line_total );

			$items[] = array(
				'external_item_id' => $external_item_id,
				'name'             => $name,
				'variant'          => $variant,
				'category_1'       => $this->primary_category( $product ),
				'quantity'         => $quantity,
				'unit_price'       => $unit_price,
				'discount'         => $discount,
				'position'         => $position,
			);

			$position++;
		}

		return $items;
	}

	/**
	 * Resolve the variant-unique identity for a product/variation.
	 *
	 * @param WC_Product|false $product Product (the variation, when applicable).
	 * @return array{0:string,1:string,2:string|null} [external_item_id, base name, variant label|null]
	 */
	private function product_identity( $product ) {
		if ( ! $product ) {
			return array( '', '', null );
		}

		$sku              = $product->get_sku();
		$external_item_id = ! empty( $sku ) ? $sku : (string) $product->get_id();

		$variant      = null;
		$base_product = $product;

		if ( $product->is_type( 'variation' ) ) {
			$formatted = wc_get_formatted_variation( $product, true );
			$variant   = '' !== $formatted ? $formatted : null;
			$parent    = wc_get_product( $product->get_parent_id() );
			if ( $parent ) {
				$base_product = $parent;
			}
		}

		return array( $external_item_id, $base_product->get_name(), $variant );
	}

	/**
	 * First product category name, if any.
	 *
	 * @param WC_Product|false $product Product object.
	 * @return string|null
	 */
	private function primary_category( $product ) {
		if ( ! $product ) {
			return null;
		}

		$terms = get_the_terms( $product->get_id(), 'product_cat' );
		if ( empty( $terms ) || is_wp_error( $terms ) ) {
			return null;
		}

		$first = reset( $terms );
		return $first ? $first->name : null;
	}

	/**
	 * Get WooCommerce order statuses on which we will send request to app.
	 *
	 * @return array
	 */
	public static function track_order_statuses() {
		$statuses = array( 'processing', 'completed' );

		return apply_filters( 'simplestats_woocommerce_purchase_order_statuses', $statuses );
	}
}
