<?php
/**
 * Shared abstract base for SimpleStats tracking providers.
 *
 * Both ecommerce purchase tracking and forms conversion tracking inherit from
 * this class so they share a single implementation of the outbound app POST.
 */

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

abstract class SimpleStats_Tracking_Base {

	/**
	 * Singleton instances, keyed by called class.
	 *
	 * @var static[]
	 */
	private static $instances = array();

	/**
	 * Full app URL for the pixel-track endpoint, including pixel key.
	 *
	 * @var string|null
	 */
	protected $request_url;

	/**
	 * Get instance of called class.
	 *
	 * @return static
	 */
	public static function get_instance() {
		$class_name = get_called_class();

		if ( ! isset( self::$instances[ $class_name ] ) ) {
			self::$instances[ $class_name ] = new static();
		}

		return self::$instances[ $class_name ];
	}

	/**
	 * Constructor.
	 */
	protected function __construct() {
		$this->request_url = $this->backend_request_url();

		$this->hooks();
	}

	/**
	 * Subclass hook registration entry point.
	 */
	abstract protected function hooks();

	/**
	 * Build the pixel-track endpoint URL for this site.
	 *
	 * Reads pixel_key from the canonical `simplestats_connection` option (the
	 * same source as `simplestats_is_connected()` and the frontend pixel
	 * script). The legacy standalone `simplestats_pixel_key` option is not
	 * populated by the v2 connect flow.
	 *
	 * @return string|null
	 */
	private function backend_request_url() {
		$connection = function_exists( 'simplestats_get_connection' ) ? simplestats_get_connection() : array();
		$pixel_key  = is_array( $connection ) && ! empty( $connection['pixel_key'] ) ? $connection['pixel_key'] : '';

		if ( empty( $pixel_key ) ) {
			return null;
		}

		return simplestats_get_app_url() . '/pixel-track/' . rawurlencode( $pixel_key );
	}

	/**
	 * Forward a tracked event to the SimpleStats app.
	 *
	 * Expected $args shape (goal conversions):
	 *   [
	 *     'type'               => string,              // required, e.g. 'goal_conversion'
	 *     'goal_name'          => string,              // required
	 *     'conversion_item_id' => string|int|null,     // optional
	 *     'conversion_value'   => float|int|null,      // optional
	 *     'url'                => string,              // optional, defaults to home_url()
	 *     'extra'              => array,               // optional, merged into top-level payload
	 *   ]
	 *
	 * Structured custom events (Goals V2, matched by Event goals):
	 *   [
	 *     'type' => 'event',
	 *     'url'  => string,                            // optional, defaults to home_url()
	 *     'data' => [
	 *       'category' => string,                      // required
	 *       'action'   => string,                      // required
	 *       'label'    => string|null,                 // optional
	 *       'value'    => float|null,                  // optional numeric
	 *       'path'     => string,                      // page path, e.g. '/contact/'
	 *     ],
	 *   ]
	 *
	 * @param array $args Event data.
	 * @return array|WP_Error|false wp_remote_post() result, or false if precondition failed.
	 */
	protected function send_request( array $args ) {
		// No endpoint configured — site is not connected.
		if ( empty( $this->request_url ) ) {
			return false;
		}

		$is_transaction    = isset( $args['type'] ) && 'transaction' === $args['type'];
		$is_cart_event     = isset( $args['type'] ) && 'cart_event' === $args['type'];
		$is_begin_checkout = isset( $args['type'] ) && 'begin_checkout' === $args['type'];
		$is_event          = isset( $args['type'] ) && 'event' === $args['type'];

		// The pixel's own session cookies. Read on public, un-nonced page loads by
		// design — they are opaque identifiers the pixel script set, they are
		// sanitized on the way in, and they only ever travel back out to the app
		// as attribution hints, so there is no nonce to verify here.
		// phpcs:disable WordPress.Security.NonceVerification.Recommended, WordPressVIPMinimum.Variables.RestrictedVariables.cache_constraints___COOKIE
		$visitor_uuid               = isset( $_COOKIE['visitor_uuid'] ) ? sanitize_text_field( wp_unslash( $_COOKIE['visitor_uuid'] ) ) : '';
		$visitor_session_uuid       = isset( $_COOKIE['visitor_session_uuid'] ) ? sanitize_text_field( wp_unslash( $_COOKIE['visitor_session_uuid'] ) ) : '';
		$visitor_session_event_uuid = isset( $_COOKIE['visitor_session_event_uuid'] ) ? sanitize_text_field( wp_unslash( $_COOKIE['visitor_session_event_uuid'] ) ) : '';
		// phpcs:enable WordPress.Security.NonceVerification.Recommended, WordPressVIPMinimum.Variables.RestrictedVariables.cache_constraints___COOKIE

		if ( $is_transaction ) {
			// Transactions are sent server-side (incl. admin refunds / manual
			// completion) where no visitor cookies exist. Send regardless; pass
			// the session uuids only when present so the backend can attribute.
			if ( empty( $args['transaction'] ) || ! is_array( $args['transaction'] ) ) {
				return false;
			}

			$payload_array = array(
				'type'        => 'transaction',
				'url'         => home_url(),
				'transaction' => $args['transaction'],
			);

			// Attribution session precedence: UUIDs captured on the order at
			// checkout (passed via $args['session']) win, so attribution survives
			// a later admin-side status change. Live cookies are only a front-end
			// fallback — an admin's own cookies must never attribute a customer's
			// order.
			$session_uuids = ( ! empty( $args['session'] ) && is_array( $args['session'] ) ) ? $args['session'] : array();

			$attr_visitor = ! empty( $session_uuids['visitor_uuid'] ) ? $session_uuids['visitor_uuid'] : '';
			$attr_session = ! empty( $session_uuids['session_uuid'] ) ? $session_uuids['session_uuid'] : '';
			$attr_event   = ! empty( $session_uuids['event_uuid'] ) ? $session_uuids['event_uuid'] : '';

			if ( '' === $attr_visitor && ! is_admin() ) {
				$attr_visitor = $visitor_uuid;
				$attr_session = $visitor_session_uuid;
				$attr_event   = $visitor_session_event_uuid;
			}

			if ( ! empty( $attr_visitor ) ) {
				$payload_array['visitor_uuid'] = $attr_visitor;
			}
			if ( ! empty( $attr_session ) ) {
				$payload_array['visitor_session_uuid'] = $attr_session;
			}
			if ( ! empty( $attr_event ) ) {
				$payload_array['visitor_session_event_uuid'] = $attr_event;
			}
		} elseif ( $is_cart_event ) {
			// Add-to-cart events fire on the front end while pixel cookies exist.
			// Send regardless; attach the live session uuids only when present so
			// the backend can attribute (best-effort).
			if ( empty( $args['cart'] ) || ! is_array( $args['cart'] ) ) {
				return false;
			}

			$payload_array = array(
				'type' => 'cart_event',
				'url'  => home_url(),
				'cart' => $args['cart'],
			);

			if ( ! empty( $visitor_uuid ) ) {
				$payload_array['visitor_uuid'] = $visitor_uuid;
			}
			if ( ! empty( $visitor_session_uuid ) ) {
				$payload_array['visitor_session_uuid'] = $visitor_session_uuid;
			}
			if ( ! empty( $visitor_session_event_uuid ) ) {
				$payload_array['visitor_session_event_uuid'] = $visitor_session_event_uuid;
			}
		} elseif ( $is_begin_checkout ) {
			// Reached-checkout events fire on the front end while pixel cookies
			// exist. Send regardless; attach the live session uuids only when
			// present so the backend can attribute (best-effort).
			if ( empty( $args['checkout'] ) || ! is_array( $args['checkout'] ) ) {
				return false;
			}

			$payload_array = array(
				'type'     => 'begin_checkout',
				'url'      => home_url(),
				'checkout' => $args['checkout'],
			);

			if ( ! empty( $visitor_uuid ) ) {
				$payload_array['visitor_uuid'] = $visitor_uuid;
			}
			if ( ! empty( $visitor_session_uuid ) ) {
				$payload_array['visitor_session_uuid'] = $visitor_session_uuid;
			}
			if ( ! empty( $visitor_session_event_uuid ) ) {
				$payload_array['visitor_session_event_uuid'] = $visitor_session_event_uuid;
			}
		} elseif ( $is_event ) {
			// Structured custom event (Goals V2): Category / Action / Label / Value,
			// matched by the app's Event goals. Requires the pixel's visitor +
			// session cookies; the event becomes its own sessions_events row, so a
			// FRESH event uuid is sent rather than reusing the page's.
			if ( empty( $visitor_uuid ) || empty( $visitor_session_uuid ) ) {
				return false;
			}

			if ( empty( $args['data'] ) || ! is_array( $args['data'] ) || empty( $args['data']['category'] ) || empty( $args['data']['action'] ) ) {
				return false;
			}

			$payload_array = array(
				'visitor_uuid'               => $visitor_uuid,
				'visitor_session_uuid'       => $visitor_session_uuid,
				'visitor_session_event_uuid' => wp_generate_uuid4(),
				'type'                       => 'event',
				'url'                        => isset( $args['url'] ) ? $args['url'] : home_url(),
				'data'                       => array(
					'category' => (string) $args['data']['category'],
					'action'   => (string) $args['data']['action'],
					'label'    => isset( $args['data']['label'] ) && '' !== $args['data']['label'] ? (string) $args['data']['label'] : null,
					'value'    => isset( $args['data']['value'] ) && is_numeric( $args['data']['value'] ) ? (float) $args['data']['value'] : null,
					'path'     => ! empty( $args['data']['path'] ) ? (string) $args['data']['path'] : '/',
				),
			);
		} else {
			// Required visitor cookies must be present (set by the pixel script).
			if ( empty( $visitor_uuid ) || empty( $visitor_session_uuid ) || empty( $visitor_session_event_uuid ) ) {
				return false;
			}

			// Required args.
			if ( empty( $args['type'] ) || empty( $args['goal_name'] ) ) {
				return false;
			}

			$payload_array = array(
				'visitor_uuid'               => $visitor_uuid,
				'visitor_session_uuid'       => $visitor_session_uuid,
				'visitor_session_event_uuid' => $visitor_session_event_uuid,
				'type'                       => $args['type'],
				'goal_name'                  => $args['goal_name'],
				'url'                        => isset( $args['url'] ) ? $args['url'] : home_url(),
				'conversion_value'           => isset( $args['conversion_value'] ) ? $args['conversion_value'] : null,
				'conversion_item_id'         => isset( $args['conversion_item_id'] ) ? $args['conversion_item_id'] : null,
			);

			if ( ! empty( $args['extra'] ) && is_array( $args['extra'] ) ) {
				foreach ( $args['extra'] as $key => $value ) {
					// Don't let `extra` overwrite the canonical fields above.
					if ( ! array_key_exists( $key, $payload_array ) ) {
						$payload_array[ $key ] = $value;
					}
				}
			}
		}

		$post_args = array(
			'body'     => array( 'data' => wp_json_encode( $payload_array ) ),
			'headers'  => array(
				// Identify honestly as the plugin. Never impersonate a browser:
				// this is a server-to-server call, and a spoofed UA reads as
				// evasion to anyone auditing outbound traffic.
				'User-Agent' => 'SimpleStats-WordPress/' . SIMPLESTATS_PLUGIN_VERSION . ' (' . home_url() . ')',
			),
			/*
			 * Fire-and-forget for everything except transactions.
			 *
			 * No caller inspects the response body of a cart, checkout or custom
			 * event, so blocking on one only buys latency. It matters most for
			 * `event`: that path is reached from the PUBLIC forms endpoint, where
			 * a blocking request means every unauthenticated hit occupies a PHP
			 * worker for the duration of a third-party round trip — a cheap way
			 * for someone to exhaust the pool. Transactions stay blocking so
			 * add_order() only writes its `_simplestats_is_in_app` guard meta
			 * after the request has actually been handed off.
			 */
			'blocking' => ! $is_cart_event && ! $is_begin_checkout && ! $is_event,
		);

		return wp_remote_post( $this->request_url, $post_args );
	}
}
