<?php
/**
 * Storefront client tracking runtime.
 *
 * @package Carticy\Conversions\Client
 */

declare(strict_types=1);

namespace Carticy\Conversions\Client;

use Carticy\Conversions\Capture\OrderCaptureService;
use Carticy\Conversions\Domain\AttributionData;
use Carticy\Conversions\Domain\ConversionEvent;
use Carticy\Conversions\Domain\ConversionEventType;
use Carticy\Conversions\Domain\IdentityData;
use Carticy\Conversions\Storage\OrderMetaStore;
use Carticy\Conversions\Support\ScriptAttributes;

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

/**
 * Hydrates consent-gated browser events from stored order-paid snapshots.
 */
final class StorefrontTrackingService {

	/**
	 * Storage.
	 *
	 * @var OrderMetaStore
	 */
	private OrderMetaStore $store;

	/**
	 * Capture service.
	 *
	 * @var OrderCaptureService
	 */
	private OrderCaptureService $capture_service;

	/**
	 * Serializer.
	 *
	 * @var ClientEventSerializer
	 */
	private ClientEventSerializer $serializer;

	/**
	 * Events collected for the current page render.
	 *
	 * @var array<int, array<string, mixed>>
	 */
	private array $events = array();

	/**
	 * Constructor.
	 *
	 * @param OrderMetaStore        $store Storage.
	 * @param OrderCaptureService   $capture_service Capture service.
	 * @param ClientEventSerializer $serializer Serializer.
	 */
	public function __construct( OrderMetaStore $store, OrderCaptureService $capture_service, ClientEventSerializer $serializer ) {
		$this->store           = $store;
		$this->capture_service = $capture_service;
		$this->serializer      = $serializer;
	}

	/**
	 * Register storefront hooks.
	 */
	public function register_hooks(): void {
		add_action( 'template_redirect', array( $this, 'hydrate_order_received_purchase_from_request' ), 5 );
		add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_storefront_script' ), 30 );
		add_filter( 'script_loader_tag', array( $this, 'script_loader_tag' ), 10, 3 );
		add_filter( 'woocommerce_loop_add_to_cart_args', array( $this, 'add_to_cart_button_args' ), 10, 2 );
		add_action( 'woocommerce_before_add_to_cart_button', array( $this, 'render_single_product_data_node' ) );
	}

	/**
	 * Render an invisible data node inside the single-product cart form so
	 * non-AJAX add-to-cart submits carry real value/currency/name (zero CLS:
	 * the node is hidden and empty).
	 */
	public function render_single_product_data_node(): void {
		if ( ! function_exists( 'wc_get_product' ) ) {
			return;
		}

		global $product;
		if ( ! is_object( $product ) || ! method_exists( $product, 'get_name' ) ) {
			return;
		}

		$price = function_exists( 'wc_get_price_to_display' ) ? (float) wc_get_price_to_display( $product ) : 0.0;
		printf(
			'<span class="carticy-conversions-product-data" hidden data-carticy-price="%s" data-carticy-name="%s" data-carticy-currency="%s"></span>',
			esc_attr( $price > 0 ? (string) $price : '' ),
			esc_attr( (string) $product->get_name() ),
			esc_attr( function_exists( 'get_woocommerce_currency' ) ? (string) get_woocommerce_currency() : '' )
		);
	}

	/**
	 * Hydrate order-received purchase data from the current WooCommerce request.
	 *
	 * A VALID order key is mandatory. Without this gate, any visitor
	 * enumerating order ids could read the purchase payload (items, totals)
	 * from the page config AND poison the buyer's stored identity snapshot
	 * with their own cookies via the returning-browser refresh.
	 */
	public function hydrate_order_received_purchase_from_request(): void {
		if ( function_exists( 'is_order_received_page' ) && ! is_order_received_page() ) {
			return;
		}

		// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- WooCommerce order-received keys are public URL tokens; validated through WooCommerce below.
		if ( ! isset( $_GET['key'] ) || ! function_exists( 'wc_get_order_id_by_order_key' ) ) {
			return;
		}

		// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- WooCommerce validates the order key and the value is sanitized before lookup.
		$key_order_id = absint( wc_get_order_id_by_order_key( sanitize_text_field( (string) wp_unslash( $_GET['key'] ) ) ) );
		if ( $key_order_id <= 0 ) {
			return;
		}

		$path_order_id = function_exists( 'get_query_var' ) ? absint( get_query_var( 'order-received' ) ) : 0;
		if ( $path_order_id > 0 && $path_order_id !== $key_order_id ) {
			return;
		}

		// The order id comes FROM the validated key, never from the path alone.
		$this->hydrate_order_received_purchase( $key_order_id );
	}

	/**
	 * Render product data on archive add-to-cart buttons so client
	 * add_to_cart events carry real value/currency/name.
	 *
	 * @param mixed $args Button args.
	 * @param mixed $product Product object.
	 * @return mixed
	 */
	public function add_to_cart_button_args( $args, $product ) {
		if ( ! is_array( $args ) || ! is_object( $product ) ) {
			return $args;
		}

		$attributes = isset( $args['attributes'] ) && is_array( $args['attributes'] ) ? $args['attributes'] : array();

		$price = function_exists( 'wc_get_price_to_display' ) ? (float) wc_get_price_to_display( $product ) : 0.0;
		if ( $price > 0 ) {
			$attributes['data-carticy-price'] = (string) $price;
		}
		if ( method_exists( $product, 'get_name' ) ) {
			$attributes['data-carticy-name'] = (string) $product->get_name();
		}
		if ( function_exists( 'get_woocommerce_currency' ) ) {
			$attributes['data-carticy-currency'] = (string) get_woocommerce_currency();
		}

		$args['attributes'] = $attributes;

		return $args;
	}

	/**
	 * Hydrate purchase from the stored paid-order event.
	 *
	 * This is only the UI return point. The purchase itself must already
	 * exist as an order-paid event, otherwise nothing is emitted.
	 *
	 * @param int|string $order_id Order ID.
	 */
	public function hydrate_order_received_purchase( $order_id ): void {
		$order_id = absint( $order_id );
		if ( $order_id <= 0 || ! function_exists( 'wc_get_order' ) ) {
			return;
		}

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

		$event_id = ConversionEvent::deterministic_event_id( ConversionEventType::PURCHASE, $order_id, (string) $order_id );
		$event    = $this->store->get_event( $order, $event_id );
		if ( null === $event ) {
			do_action( 'carticy_conversions_missing_client_purchase_event', $order_id, $event_id );
			return;
		}

		$settings = ClientTrackingSettings::from_options();
		if ( ! $this->event_allows_any_active_client_provider( $event, $settings ) ) {
			return;
		}

		$event          = $this->refresh_returning_browser_snapshots( $order, $event, $settings );
		$this->events[] = $this->serializer->serialize( $event );
	}

	/**
	 * Enqueue browser dispatcher only when a configured destination or hydrated event exists.
	 */
	public function enqueue_storefront_script(): void {
		if ( ! defined( 'CARTICY_CONVERSIONS_URL' ) || ! defined( 'CARTICY_CONVERSIONS_PATH' ) ) {
			return;
		}

		$settings = ClientTrackingSettings::from_options();
		if ( ! $settings->has_active_destinations() ) {
			return;
		}
		$context = $this->context();
		if ( array() === $this->events && ! $this->is_relevant_context( $context ) ) {
			return;
		}

		$path = CARTICY_CONVERSIONS_PATH . 'assets/js/dist/storefront-tracking.js';
		if ( ! file_exists( $path ) ) {
			return;
		}

		wp_enqueue_script(
			'carticy-conversions-storefront-tracking',
			CARTICY_CONVERSIONS_URL . 'assets/js/dist/storefront-tracking.js',
			array(),
			defined( 'CARTICY_CONVERSIONS_VERSION' ) ? CARTICY_CONVERSIONS_VERSION : (string) filemtime( $path ),
			true
		);
		ScriptAttributes::exclude_from_optimizers( 'carticy-conversions-storefront-tracking' );

		$config = $settings->storefront_config( $this->events, $context );
		wp_localize_script( 'carticy-conversions-storefront-tracking', 'carticyConversionsTracking', $config );
		wp_add_inline_script(
			'carticy-conversions-storefront-tracking',
			'window.carticyConversionsTracking = window.carticyConversionsTracking || carticyConversionsTracking;',
			'before'
		);
	}

	/**
	 * Add async/defer/cache-optimizer guard attributes to the tracking script.
	 *
	 * @param string $tag Script tag.
	 * @param string $handle Script handle.
	 * @param string $src Script source.
	 * @return string
	 */
	public function script_loader_tag( string $tag, string $handle, string $src ): string {
		unset( $src );
		if ( 'carticy-conversions-storefront-tracking' !== $handle ) {
			return $tag;
		}

		// Inject into the existing tag instead of rebuilding it so CSP
		// nonces/attributes added by other filters survive.
		return ScriptAttributes::with_attributes(
			$tag,
			array(
				'async'            => '',
				'defer'            => '',
				'data-cfasync'     => 'false',
				'data-no-optimize' => '1',
				'data-no-minify'   => '1',
			)
		);
	}

	/**
	 * Refresh order-bound identity and the stored purchase snapshot from the returning browser.
	 *
	 * Refresh is per provider, not all-or-nothing: with ga4 granted and meta
	 * denied, harvesting the visitor's _fbp/_fbc onto the order would persist
	 * Meta identifiers a denied consent forbids using.
	 *
	 * @param object                 $order Order.
	 * @param ConversionEvent        $event Stored event.
	 * @param ClientTrackingSettings $settings Settings.
	 * @return ConversionEvent
	 */
	private function refresh_returning_browser_snapshots( object $order, ConversionEvent $event, ClientTrackingSettings $settings ): ConversionEvent {
		$consent        = $this->store->get_consent( $order );
		$google_allowed = ( $settings->provider_enabled( 'ga4' ) && $consent->allows_service_send( 'ga4' ) )
			|| ( $settings->provider_enabled( 'google_ads' ) && $consent->allows_service_send( 'google-ads' ) );
		$meta_allowed   = $settings->provider_enabled( 'meta' ) && $consent->allows_service_send( 'meta' );

		$refresh_keys = array();
		if ( $google_allowed ) {
			$refresh_keys = array_merge( $refresh_keys, array( 'ga_client_id', 'ga_session_id', 'ga_session_captured_at' ) );
		}
		if ( $meta_allowed ) {
			$refresh_keys = array_merge( $refresh_keys, array( 'fbp', 'fbc' ) );
		}

		$current_identity = $this->store->get_identity( $order )->to_array();
		$fresh_identity   = $this->capture_service->build_identity( $order )->to_array();
		foreach ( $refresh_keys as $key ) {
			if ( ! empty( $fresh_identity[ $key ] ) ) {
				$current_identity[ $key ] = $fresh_identity[ $key ];
			}
		}

		$current_attribution = $this->store->get_attribution( $order )->to_array();
		$fresh_attribution   = $this->capture_service->build_attribution()->to_array();
		if ( empty( $current_attribution['first_touch'] ) && ! empty( $fresh_attribution['first_touch'] ) ) {
			$current_attribution['first_touch'] = $fresh_attribution['first_touch'];
		}
		if ( ! empty( $fresh_attribution['last_touch'] ) ) {
			$current_attribution['last_touch'] = $fresh_attribution['last_touch'];
		}

		$identity    = IdentityData::from_array( $current_identity );
		$attribution = AttributionData::from_array( $current_attribution );

		$this->store->save_identity( $order, $identity, false );
		$this->store->save_attribution( $order, $attribution, false );

		$data                = $event->to_array();
		$data['identity']    = $identity->to_array();
		$data['attribution'] = $attribution->to_array();
		$refreshed           = ConversionEvent::from_array( $data );
		$this->store->save_event( $order, $refreshed, true );

		return $refreshed;
	}

	/**
	 * Check whether the stored consent snapshot permits any active browser destination.
	 *
	 * @param ConversionEvent        $event Event.
	 * @param ClientTrackingSettings $settings Settings.
	 * @return bool
	 */
	private function event_allows_any_active_client_provider( ConversionEvent $event, ClientTrackingSettings $settings ): bool {
		$consent = $event->to_array()['consent'] ?? array();
		if ( ! is_array( $consent ) ) {
			return false;
		}

		foreach ( array( 'ga4', 'google_ads', 'meta' ) as $provider ) {
			if ( ! $settings->provider_enabled( $provider ) ) {
				continue;
			}

			$service = 'google_ads' === $provider ? 'google-ads' : $provider;
			if ( isset( $consent['services'][ $service ] ) ) {
				if ( 'granted' === $consent['services'][ $service ] ) {
					return true;
				}
				continue;
			}

			if ( 'granted' === ( $consent['state'] ?? 'unset' ) ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Build page context for the JS event harness.
	 *
	 * @return array<string, mixed>
	 */
	private function context(): array {
		return array(
			'isWooCommerce'   => function_exists( 'is_woocommerce' ) ? (bool) is_woocommerce() : false,
			'isCart'          => function_exists( 'is_cart' ) ? (bool) is_cart() : false,
			'isCheckout'      => function_exists( 'is_checkout' ) ? (bool) is_checkout() : false,
			'isOrderReceived' => function_exists( 'is_order_received_page' ) ? (bool) is_order_received_page() : false,
			'currency'        => function_exists( 'get_woocommerce_currency' ) ? (string) get_woocommerce_currency() : '',
			'cart'            => $this->cart_context(),
		);
	}

	/**
	 * Cart totals and items so funnel events (begin_checkout) carry real value.
	 *
	 * @return array<string, mixed>
	 */
	private function cart_context(): array {
		if ( ! function_exists( 'WC' ) ) {
			return array();
		}

		$wc   = WC();
		$cart = is_object( $wc ) && isset( $wc->cart ) && is_object( $wc->cart ) ? $wc->cart : null;
		if ( null === $cart || ! method_exists( $cart, 'get_cart' ) ) {
			return array();
		}

		$items = array();
		foreach ( (array) $cart->get_cart() as $item ) {
			if ( ! is_array( $item ) ) {
				continue;
			}

			$product_id = (int) ( ! empty( $item['variation_id'] ) ? $item['variation_id'] : ( $item['product_id'] ?? 0 ) );
			if ( $product_id <= 0 ) {
				continue;
			}

			$quantity   = max( 1, (int) ( $item['quantity'] ?? 1 ) );
			$line_total = (float) ( $item['line_total'] ?? 0 );
			$items[]    = array(
				'id'       => (string) $product_id,
				'quantity' => $quantity,
				// Defensive division guard kept although max( 1, ... ) already bounds quantity.
				// @phpstan-ignore-next-line
				'price'    => $quantity > 0 ? round( $line_total / $quantity, 2 ) : 0,
			);
		}

		$total = 0.0;
		if ( method_exists( $cart, 'get_total' ) ) {
			$total = (float) $cart->get_total( 'edit' );
		}

		return array(
			'value' => $total,
			'items' => $items,
			'hash'  => method_exists( $cart, 'get_cart_hash' ) ? (string) $cart->get_cart_hash() : '',
		);
	}

	/**
	 * Determine whether the current page can produce Woo/browser events.
	 *
	 * @param array<string, mixed> $context Page context.
	 * @return bool
	 */
	private function is_relevant_context( array $context ): bool {
		foreach ( array( 'isWooCommerce', 'isCart', 'isCheckout', 'isOrderReceived' ) as $key ) {
			if ( ! empty( $context[ $key ] ) ) {
				return true;
			}
		}

		return false;
	}
}
