<?php
/**
 * Cache module — Web Vitals RUM collector
 *
 * Owns the REST endpoint that receives Core Web Vitals samples from real
 * visitors and inserts them into the be_web_vitals table. Public endpoint
 * (no auth) protected by:
 *
 *   - Rate limit (60 inserts / IP / minute via transient)
 *   - Strict input validation (enum metric, bounded value, capped url length)
 *   - Sample-rate honouring (server can re-check the rate cookie)
 *
 * Frontend script `web-vitals-rum.js` is enqueued only on opt-in (RGPD).
 *
 * @package BlaminhorEssentials
 */

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

class Blaminhor_Essentials_Cache_Webvitals_Collector {

	const NAMESPACE_REST = 'blaminhor-essentials/v1';
	const ENDPOINT       = '/web-vitals';

	const ALLOWED_METRICS = array( 'LCP', 'INP', 'CLS', 'FCP', 'TTFB', 'FID' );
	const ALLOWED_DEVICES = array( 'mobile', 'desktop', 'tablet' );

	/** @var Blaminhor_Essentials_Module_Cache */
	private $parent;

	public function __construct( Blaminhor_Essentials_Module_Cache $parent ) {
		$this->parent = $parent;
	}

	public function register_hooks() {
		add_action( 'rest_api_init', array( $this, 'register_rest_route' ) );
		add_action( 'wp_enqueue_scripts', array( $this, 'maybe_enqueue_rum' ) );
	}

	public function register_rest_route() {
		register_rest_route(
			self::NAMESPACE_REST,
			self::ENDPOINT,
			array(
				'methods'             => 'POST',
				'callback'            => array( $this, 'rest_collect' ),
				'permission_callback' => '__return_true',
				'args'                => array(
					'metric' => array( 'required' => true, 'type' => 'string' ),
					'value'  => array( 'required' => true, 'type' => 'number' ),
					'url'    => array( 'required' => true, 'type' => 'string' ),
					'device' => array( 'required' => false, 'type' => 'string' ),
					'rating' => array( 'required' => false, 'type' => 'string' ),
					'navigation_type' => array( 'required' => false, 'type' => 'string' ),
				),
			)
		);
	}

	public function rest_collect( WP_REST_Request $request ) {
		// Bail out cheaply if RUM is disabled. Saves DB writes from cached pages
		// that still carry the script after the user toggled off.
		if ( ! $this->parent->get_setting( 'webvitals_enabled', false ) ) {
			return new WP_REST_Response( array( 'ok' => true, 'noop' => true ), 200 );
		}

		// Rate limit per IP.
		$ip      = $this->client_ip();
		$key     = 'blaminhor_cache_webvitals_rate_' . md5( $ip );
		$current = (int) get_transient( $key );
		if ( $current > 60 ) {
			return new WP_REST_Response( array( 'ok' => false, 'reason' => 'rate' ), 429 );
		}
		set_transient( $key, $current + 1, MINUTE_IN_SECONDS );

		$metric = strtoupper( (string) $request->get_param( 'metric' ) );
		$value  = (float) $request->get_param( 'value' );
		$url    = (string) $request->get_param( 'url' );
		$device = strtolower( (string) $request->get_param( 'device' ) );
		$rating = (string) $request->get_param( 'rating' );
		$nav    = (string) $request->get_param( 'navigation_type' );

		if ( ! in_array( $metric, self::ALLOWED_METRICS, true ) ) {
			return new WP_REST_Response( array( 'ok' => false, 'reason' => 'metric' ), 400 );
		}
		if ( ! in_array( $device, self::ALLOWED_DEVICES, true ) ) {
			$device = 'desktop';
		}
		if ( ! in_array( $rating, array( 'good', 'needs-improvement', 'poor', '' ), true ) ) {
			$rating = '';
		}
		// Cap absurdities. CLS is unit-less; everything else is in ms.
		if ( $metric === 'CLS' ) {
			if ( $value < 0 || $value > 5 ) {
				return new WP_REST_Response( array( 'ok' => false, 'reason' => 'value' ), 400 );
			}
		} else {
			if ( $value < 0 || $value > 60000 ) {
				return new WP_REST_Response( array( 'ok' => false, 'reason' => 'value' ), 400 );
			}
		}

		$url = esc_url_raw( $url );
		if ( '' === $url || strlen( $url ) > 2048 ) {
			return new WP_REST_Response( array( 'ok' => false, 'reason' => 'url' ), 400 );
		}

		// Insert.
		global $wpdb;
		$table = $wpdb->prefix . 'be_web_vitals';
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
		$wpdb->insert(
			$table,
			array(
				'blog_id'         => is_multisite() ? get_current_blog_id() : 0,
				'url_hash'        => sha1( $url ),
				'url'             => $url,
				'device'          => $device,
				'metric'          => $metric,
				'value'           => $value,
				'rating'          => '' === $rating ? null : $rating,
				'navigation_type' => '' === $nav ? null : substr( $nav, 0, 20 ),
				'user_agent_hash' => sha1( (string) $request->get_header( 'user_agent' ) . $ip ),
				'recorded_at'     => current_time( 'mysql', true ),
			),
			array( '%d', '%s', '%s', '%s', '%s', '%f', '%s', '%s', '%s', '%s' )
		);

		return new WP_REST_Response( array( 'ok' => true ), 204 );
	}

	public function maybe_enqueue_rum() {
		// Real-User Monitoring only matters for actual public visitors — an
		// administrator browsing the site does not represent the user
		// population the dashboard is meant to track. Replaces the previous
		// is_admin()-only check.
		if ( $this->parent->should_skip_runtime_optimizations() ) {
			return;
		}
		if ( ! $this->parent->get_setting( 'webvitals_enabled', false ) ) {
			return;
		}
		// Sample rate gating server-side via cookie (browser also re-checks).
		$sample = (int) $this->parent->get_setting( 'webvitals_sample_rate', 100 );
		if ( $sample <= 0 ) {
			return;
		}

		wp_enqueue_script(
			'blaminhor-essentials-cache-rum',
			BLAMINHOR_ESSENTIALS_PLUGIN_URL . 'assets/js/frontend/web-vitals-rum.js',
			array(),
			BLAMINHOR_ESSENTIALS_VERSION,
			array( 'in_footer' => true, 'strategy' => 'defer' )
		);
		wp_localize_script( 'blaminhor-essentials-cache-rum', 'blaminhorCacheRum', array(
			'endpoint'   => esc_url_raw( rest_url( self::NAMESPACE_REST . self::ENDPOINT ) ),
			'sampleRate' => max( 0, min( 100, $sample ) ),
		) );
	}

	private function client_ip() {
		$candidates = array( 'HTTP_CF_CONNECTING_IP', 'HTTP_X_FORWARDED_FOR', 'REMOTE_ADDR' );
		foreach ( $candidates as $h ) {
			if ( ! empty( $_SERVER[ $h ] ) ) {
				$ip = (string) $_SERVER[ $h ];
				if ( false !== strpos( $ip, ',' ) ) {
					$ip = trim( explode( ',', $ip )[0] );
				}
				return sanitize_text_field( $ip );
			}
		}
		return '0.0.0.0';
	}
}
