<?php
/**
 * Sales tracker: attributes orders to AI chat conversations.
 *
 * @package KwiroAISalesAgent
 */

defined( 'ABSPATH' ) || exit;

/**
 * Class Kwiro_Sales_Tracker
 *
 * Tracks WooCommerce order completions and checks for AI chat attribution.
 * Two attribution types:
 * - Direct: Customer clicked a product link in the chat and bought it.
 * - Assisted: Customer had a chat session and bought within 24 hours.
 */
class Kwiro_Sales_Tracker {

	/**
	 * Attribution window in seconds (24 hours).
	 *
	 * @var int
	 */
	const ATTRIBUTION_WINDOW = 86400;

	/**
	 * API Client instance.
	 *
	 * @var Kwiro_API_Client
	 */
	private $api_client;

	/**
	 * Constructor.
	 *
	 * @param Kwiro_API_Client $api_client API client instance.
	 */
	public function __construct( $api_client ) {
		$this->api_client = $api_client;

		$this->register_hooks();
	}

	/**
	 * Register WooCommerce order hooks.
	 */
	private function register_hooks() {
		// `payment_complete` covers paid online gateways (Stripe/PayPal). It does NOT fire for
		// Cash-on-Delivery / Bank Transfer — those orders move straight to `processing` (or
		// `on-hold`) without payment confirmation, so we hook those statuses too. Status
		// `completed` is kept for stores that auto-complete (virtual goods) or admins who mark
		// orders complete manually. `_kwiro_tracked` order meta dedupes multi-hook fires.
		add_action( 'woocommerce_order_status_processing', array( $this, 'on_order_processing' ) );
		add_action( 'woocommerce_order_status_on-hold', array( $this, 'on_order_on_hold' ) );
		add_action( 'woocommerce_order_status_completed', array( $this, 'on_order_completed' ) );
		add_action( 'woocommerce_payment_complete', array( $this, 'on_payment_complete' ) );
		add_action( 'woocommerce_checkout_update_order_meta', array( $this, 'save_checkout_attribution' ) );
		// Block-based checkout (WC 7.5+). The `_update_order_from_request` hook is the right one
		// here — it passes both the order AND the WP_REST_Request, which is the only clean way to
		// access `$request['extensions']['kwiro_attribution']` set by our fetch interceptor below.
		// The older `_order_processed` hook only passes $order, forcing a fragile `php://input`
		// re-parse that breaks on stacks where the body has already been consumed.
		add_action( 'woocommerce_store_api_checkout_update_order_from_request', array( $this, 'save_block_checkout_attribution' ), 10, 2 );
		// Belt-and-suspenders: also hook the post-processed action with a cookie-only handler.
		// Some WC + caching combinations either don't fire `_update_order_from_request` or call
		// our handler before the order is fully reconstructed, so the meta we set there gets
		// dropped. `_order_processed` fires after the order is saved + payment processed, and
		// the kwiro_conv cookie is in $_COOKIE on every Store API request so we never need
		// $request to read it.
		add_action( 'woocommerce_store_api_checkout_order_processed', array( $this, 'save_block_attribution_from_cookie' ) );

		// Register `kwiro_attribution` as a known Store API extension namespace. Without this,
		// WC strips unregistered keys from `request['extensions']` before our hook fires — the
		// JS interceptor's payload silently disappears and `_update_order_from_request` sees an
		// empty extensions array, even though the request body contained our data. Registered
		// here (not constructor) because the StoreApi container only exists after
		// `woocommerce_blocks_loaded`.
		add_action( 'woocommerce_blocks_loaded', array( $this, 'register_blocks_extension_schema' ) );

		// Set attribution cookie from IFRAME postMessage. Enqueued (not inlined)
		// so wp.org's "all JS must be enqueued" rule is satisfied.
		add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_attribution_script' ) );
	}

	/**
	 * Handle order completion.
	 *
	 * @param int $order_id WooCommerce order ID.
	 */
	public function on_order_completed( $order_id ) {
		$this->track_order( $order_id );
	}

	/**
	 * Handle payment complete.
	 *
	 * @param int $order_id WooCommerce order ID.
	 */
	public function on_payment_complete( $order_id ) {
		$this->track_order( $order_id );
	}

	/**
	 * Handle order moved to processing (COD checkout / paid online).
	 *
	 * @param int $order_id WooCommerce order ID.
	 */
	public function on_order_processing( $order_id ) {
		$this->track_order( $order_id );
	}

	/**
	 * Handle order moved to on-hold (BACS / awaiting offline payment).
	 *
	 * @param int $order_id WooCommerce order ID.
	 */
	public function on_order_on_hold( $order_id ) {
		$this->track_order( $order_id );
	}

	/**
	 * Track an order for AI attribution.
	 *
	 * @param int $order_id WooCommerce order ID.
	 */
	public function track_order( $order_id ) {
		if ( ! $this->api_client->is_configured() ) {
			return;
		}

		$order = wc_get_order( $order_id );
		if ( ! $order ) {
			return;
		}

		// Check if already tracked.
		$already_tracked = $order->get_meta( '_kwiro_tracked' );
		if ( $already_tracked ) {
			return;
		}

		// Get attribution data from order meta or cookie.
		$attribution = $this->get_attribution( $order );
		if ( ! $attribution ) {
			return;
		}

		// Build products in order.
		$products_in_order = array();
		foreach ( $order->get_items() as $item ) {
			$product = $item->get_product();
			if ( $product ) {
				$products_in_order[] = array(
					'wc_product_id' => $product->get_id(),
					'name'          => $product->get_name(),
					'quantity'      => $item->get_quantity(),
					'total'         => (float) $item->get_total(),
				);
			}
		}

		// Send to cloud API.
		$result = $this->api_client->post(
			'/api/sync/sales-event',
			array(
				'order_id'         => (string) $order_id,
				'order_total'      => (float) $order->get_total(),
				'currency'         => $order->get_currency(),
				'attribution_type' => $attribution['type'],
				'conversation_id'  => $attribution['conversation_id'] ?? null,
				'product_ids'      => array_map(
					function ( $p ) {
						return (string) $p['wc_product_id']; },
					$products_in_order
				),
			)
		);

		if ( ! is_wp_error( $result ) ) {
			$order->update_meta_data( '_kwiro_tracked', true );
			$order->update_meta_data( '_kwiro_attribution', $attribution['type'] );
			$order->save();
		} elseif ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
			// Surface the failure to wp-content/debug.log only when the merchant
			// has WP_DEBUG_LOG turned on. Avoids spamming the PHP error log on
			// production sites that haven't opted into verbose plugin output.
			// `_kwiro_tracked` stays false on error so the next status transition
			// retries automatically.
			error_log( sprintf( 'Kwiro: sales-event POST failed for order %d: %s', $order_id, $result->get_error_message() ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
		}
	}

	/**
	 * Get attribution data for an order.
	 *
	 * @param WC_Order $order WooCommerce order.
	 * @return array|null Attribution data or null.
	 */
	public function get_attribution( $order ) {
		// Check for direct attribution (stored in order meta during checkout).
		$conversation_id = $order->get_meta( '_kwiro_conversation_id' );
		$chat_timestamp  = (int) $order->get_meta( '_kwiro_chat_timestamp' );

		// Legacy plugin builds (≤ 1.2.6) wrote `Date.now()` (milliseconds) into the cookie /
		// hidden form field, then stored the raw value as `_kwiro_chat_timestamp`. PHP `time()`
		// returns seconds, so `time() - chat_timestamp_ms` was a hugely-negative elapsed value —
		// which happened to pass `< 1800` and produce 'direct' for online-paid orders, but could
		// never resolve to 'assisted'. Detect ms-shaped values (anything > year-2286 in seconds)
		// and rescale.
		if ( $chat_timestamp > 9999999999 ) {
			$chat_timestamp = (int) ( $chat_timestamp / 1000 );
		}

		if ( $conversation_id && $chat_timestamp ) {
			$elapsed = time() - $chat_timestamp;

			// Only attribute if within the attribution window.
			if ( $elapsed >= 0 && $elapsed <= self::ATTRIBUTION_WINDOW ) {
				return array(
					'type'            => $elapsed < 1800 ? 'direct' : 'assisted',
					'conversation_id' => $conversation_id,
					'time_elapsed'    => $elapsed,
				);
			}
		}

		return null;
	}

	/**
	 * Enqueue the frontend attribution-capture script. The widget origin is
	 * passed in via wp_add_inline_script so the JS file itself stays a static
	 * asset that wp.org's "all JS must be enqueued" rule accepts.
	 */
	public function enqueue_attribution_script() {
		if ( is_admin() ) {
			return;
		}

		wp_enqueue_script(
			'kwiro-sales-tracker',
			KWIRO_PLUGIN_URL . 'assets/js/sales-tracker.js',
			array(),
			KWIRO_VERSION,
			true
		);

		// `before` position so the global is defined before sales-tracker.js
		// starts executing. wp_add_inline_script is the documented best
		// practice for non-string data (wp_localize_script is reserved for
		// translatable strings).
		wp_add_inline_script(
			'kwiro-sales-tracker',
			'window.kwiroSalesTracker = ' . wp_json_encode(
				array(
					'widgetOrigin' => esc_url( $this->get_widget_origin() ),
				)
			) . ';',
			'before'
		);
	}

	/**
	 * Get the widget origin URL for postMessage validation.
	 *
	 * @return string
	 */
	private function get_widget_origin() {
		if ( defined( 'KWIRO_WIDGET_URL' ) ) {
			return rtrim( KWIRO_WIDGET_URL, '/' );
		}
		return 'https://widget.kwiro.ai';
	}

	/**
	 * Save attribution data from checkout form to order meta.
	 * Hook: woocommerce_checkout_update_order_meta
	 *
	 * @param int $order_id Order ID.
	 */
	public function save_checkout_attribution( $order_id ) {
		// phpcs:disable WordPress.Security.NonceVerification.Missing -- WC handles nonce.
		if ( ! empty( $_POST['_kwiro_conversation_id'] ) ) {
			$order = wc_get_order( $order_id );
			if ( $order ) {
				$order->update_meta_data(
					'_kwiro_conversation_id',
					sanitize_text_field( wp_unslash( $_POST['_kwiro_conversation_id'] ) )
				);
				$order->update_meta_data(
					'_kwiro_chat_timestamp',
					absint( wp_unslash( $_POST['_kwiro_chat_timestamp'] ?? 0 ) )
				);
				$order->save();
			}
		}
		// phpcs:enable WordPress.Security.NonceVerification.Missing
	}

	/**
	 * Save attribution data from block-based checkout (WC 7.5+).
	 * Hook: woocommerce_store_api_checkout_update_order_from_request
	 *
	 * Reads `extensions.kwiro_attribution` straight off the WP_REST_Request — far more reliable
	 * than re-parsing `php://input` from the older `_order_processed` hook.
	 *
	 * @param \WC_Order        $order   WooCommerce order object.
	 * @param \WP_REST_Request $request Store API checkout request.
	 */
	public function save_block_checkout_attribution( $order, $request ) {
		$extensions  = $request['extensions'] ?? null;
		$attribution = is_array( $extensions ) ? ( $extensions['kwiro_attribution'] ?? null ) : null;

		// Cookie fallback: even with the schema registered, some WC + caching plugin
		// combinations still drop unknown extension data on the floor before the hook fires.
		// The `kwiro_conv` cookie was set by inject_attribution_script when the chat fired
		// `kwiro_attribution`, lives 24h on the storefront, and is sent with the same-origin
		// Store API request, so $_COOKIE has it server-side. Use it when extensions came up empty.
		if ( empty( $attribution['conversation_id'] ) && ! empty( $_COOKIE['kwiro_conv'] ) ) {
			// sanitize_text_field here is belt-and-suspenders — parse_kwiro_conv_cookie
			// re-sanitizes internally, but doing it inline keeps Plugin Check's static
			// analyser happy (it can't trace sanitization through a private method).
			$attribution = $this->parse_kwiro_conv_cookie(
				sanitize_text_field( wp_unslash( $_COOKIE['kwiro_conv'] ) )
			);
		}

		if ( empty( $attribution['conversation_id'] ) ) {
			return;
		}
		$order->update_meta_data(
			'_kwiro_conversation_id',
			sanitize_text_field( $attribution['conversation_id'] )
		);
		$order->update_meta_data(
			'_kwiro_chat_timestamp',
			absint( $attribution['chat_timestamp'] ?? 0 )
		);
		$order->save();
	}

	/**
	 * Cookie-only attribution save, hooked on `woocommerce_store_api_checkout_order_processed`.
	 *
	 * Dedups against the `_update_order_from_request` handler — if that ran successfully and
	 * already wrote `_kwiro_conversation_id`, this is a no-op. This is the safety net for the
	 * case where the earlier hook didn't fire or didn't stick.
	 *
	 * @param \WC_Order $order WooCommerce order object.
	 */
	public function save_block_attribution_from_cookie( $order ) {
		// Already handled upstream — nothing to do.
		if ( $order->get_meta( '_kwiro_conversation_id' ) ) {
			return;
		}
		if ( empty( $_COOKIE['kwiro_conv'] ) ) {
			return;
		}
		$parsed = $this->parse_kwiro_conv_cookie(
			sanitize_text_field( wp_unslash( $_COOKIE['kwiro_conv'] ) )
		);
		if ( ! $parsed ) {
			return;
		}
		$order->update_meta_data( '_kwiro_conversation_id', sanitize_text_field( $parsed['conversation_id'] ) );
		$order->update_meta_data( '_kwiro_chat_timestamp', absint( $parsed['chat_timestamp'] ) );
		$order->save();
	}

	/**
	 * Register `kwiro_attribution` as a Store API extension on the checkout endpoint.
	 *
	 * Called on `woocommerce_blocks_loaded` because the StoreApi DI container only exists
	 * after WC Blocks bootstrap. Wrapped in defensive guards so plugins that ship on stores
	 * with unusual WC builds (older versions, partial namespace renames in development trunks)
	 * never fatal — schema registration is a "nice to have", the cookie fallback in
	 * save_block_checkout_attribution is the real safety net.
	 */
	public function register_blocks_extension_schema() {
		if ( ! class_exists( '\\Automattic\\WooCommerce\\StoreApi\\StoreApi' )
			|| ! class_exists( '\\Automattic\\WooCommerce\\StoreApi\\Schemas\\ExtendSchema' )
			|| ! class_exists( '\\Automattic\\WooCommerce\\StoreApi\\Schemas\\V1\\CheckoutSchema' ) ) {
			return;
		}
		try {
			$container = \Automattic\WooCommerce\StoreApi\StoreApi::container();
			$extend    = $container->get( \Automattic\WooCommerce\StoreApi\Schemas\ExtendSchema::class );
			if ( ! is_object( $extend ) || ! method_exists( $extend, 'register_endpoint_data' ) ) {
				return;
			}
			$extend->register_endpoint_data(
				array(
					'endpoint'        => \Automattic\WooCommerce\StoreApi\Schemas\V1\CheckoutSchema::IDENTIFIER,
					'namespace'       => 'kwiro_attribution',
					'schema_callback' => static function () {
						return array(
							'conversation_id' => array(
								'description' => __( 'Kwiro AI chat conversation ID.', 'kwiro-ai-sales-agent' ),
								'type'        => array( 'string', 'null' ),
								'context'     => array( 'view', 'edit' ),
							),
							'chat_timestamp'  => array(
								'description' => __( 'Unix timestamp (seconds) of last chat activity.', 'kwiro-ai-sales-agent' ),
								'type'        => array( 'string', 'integer', 'null' ),
								'context'     => array( 'view', 'edit' ),
							),
						);
					},
				)
			);
		} catch ( \Throwable $e ) {
			// Non-fatal — schema registration is a "nice to have" on top of the
			// cookie fallback in save_block_attribution_from_cookie(). Newer or
			// development WC builds can break the StoreApi DI lookup without
			// breaking attribution.
			unset( $e );
		}
	}

	/**
	 * Parse the `kwiro_conv` cookie ("<uuid>|<unix-seconds>") into an attribution array.
	 *
	 * Strict UUID validation prevents a malicious 3rd-party iframe (or a forged cookie) from
	 * planting an arbitrary string as the conversation_id and claiming attribution credit.
	 *
	 * @param string $raw Raw cookie value, expected format "<uuid>|<unix-seconds>".
	 * @return array{conversation_id:string,chat_timestamp:int}|null
	 */
	private function parse_kwiro_conv_cookie( $raw ) {
		$parts = explode( '|', sanitize_text_field( $raw ), 2 );
		if ( count( $parts ) !== 2 ) {
			return null;
		}
		$conv_id = trim( $parts[0] );
		if ( ! preg_match( '/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i', $conv_id ) ) {
			return null;
		}
		return array(
			'conversation_id' => $conv_id,
			'chat_timestamp'  => (int) $parts[1],
		);
	}
}
