<?php
/**
 * Display-safe public REST API.
 *
 * @package VoidPulse
 */

namespace VoidPulse\Frontend;

use VoidPulse\Notifications\Analytics;
use VoidPulse\Infrastructure\NotificationSettings;
use VoidPulse\Infrastructure\EventRepository;
use VoidPulse\Shared\TokenRenderer;

defined( 'ABSPATH' ) || exit;

final class RestController {
	/**
	 * Register routes.
	 *
	 * @return void
	 */
	public function register() {
		register_rest_route(
			'voidpulse/v1',
			'/frontend/events',
			array(
				'methods'             => \WP_REST_Server::READABLE,
				'callback'            => array( $this, 'events' ),
				'permission_callback' => '__return_true',
			)
		);
		register_rest_route(
			'voidpulse/v1',
			'/frontend/interaction',
			array(
				'methods'             => \WP_REST_Server::CREATABLE,
				'callback'            => array( $this, 'interaction' ),
				'permission_callback' => '__return_true',
				'args'                => array(
					'metric' => array( 'required' => true, 'type' => 'string', 'enum' => array( 'impressions', 'clicks', 'dismissals' ) ),
					'client' => array( 'required' => true, 'type' => 'string', 'minLength' => 16, 'maxLength' => 64, 'pattern' => '^[a-zA-Z0-9_-]+$' ),
					'token'  => array( 'required' => true, 'type' => 'string', 'minLength' => 64, 'maxLength' => 64, 'pattern' => '^[a-f0-9]+$' ),
				),
			)
		);
	}

	/**
	 * Return safe rendered event DTOs.
	 *
	 * @return \WP_REST_Response
	 */
	public function events() {
		$campaign = ( new NotificationSettings() )->get();
		if ( ! $campaign || 'active' !== $campaign['status'] ) {
			return rest_ensure_response( array( 'events' => array() ) );
		}
		$events = ( new EventRepository() )->recent( $campaign['orders']['maximum'], $campaign['timing']['order'] );
		foreach ( $events as &$event ) {
			$event_key = isset( $event['event_key'] ) ? sanitize_text_field( $event['event_key'] ) : '';
			$event['time_ago'] = sprintf(
				/* translators: %s: human-readable elapsed time. */
				__( '%s ago', 'voidpulse-social-proof-for-woocommerce' ),
				human_time_diff( absint( $event['event_timestamp'] ), time() )
			);
			$event['message'] = TokenRenderer::render( $campaign['message'], $event );
			unset( $event['event_timestamp'], $event['customer'], $event['location'], $event['product'] );
			$event = apply_filters( 'voidpulse_notification_payload', $event, $campaign );
			$event = is_array( $event ) ? array_intersect_key( $event, array_flip( array( 'message', 'time_ago', 'product_url', 'product_image' ) ) ) : array();
			$event = array(
				'event_key'     => $event_key,
				'message'       => sanitize_text_field( isset( $event['message'] ) ? $event['message'] : '' ),
				'time_ago'      => sanitize_text_field( isset( $event['time_ago'] ) ? $event['time_ago'] : '' ),
				'product_url'   => esc_url_raw( isset( $event['product_url'] ) ? $event['product_url'] : '' ),
				'product_image' => esc_url_raw( isset( $event['product_image'] ) ? $event['product_image'] : '' ),
			);
		}
		unset( $event );
		$token = self::token( gmdate( 'YmdH' ) );
		return rest_ensure_response( array( 'events' => array_values( $events ), 'token' => $token ) );
	}

	/**
	 * Number of fixed rate-limit buckets.
	 *
	 * Counters are grouped into a fixed number of transients so that the public
	 * endpoint can never create more storage keys than this, however many
	 * distinct client identifiers are submitted.
	 */
	const RATE_BUCKETS = 32;

	/**
	 * Maximum counters retained per bucket.
	 */
	const RATE_BUCKET_ENTRIES = 100;

	/**
	 * Interactions allowed per client, per metric, per window.
	 */
	const RATE_LIMIT = 20;

	/**
	 * Return the transient name for a rate-limit bucket.
	 *
	 * @param int $bucket Bucket index.
	 * @return string
	 */
	public static function rate_bucket_key( $bucket ) {
		return 'voidpulse_rate_' . absint( $bucket );
	}

	/**
	 * Count one interaction against a bounded rate-limit bucket.
	 *
	 * @param string $client Client identifier.
	 * @param string $metric Metric name.
	 * @return bool Whether the interaction is within the limit.
	 */
	private function within_rate_limit( $client, $metric ) {
		$digest = hash_hmac( 'sha256', $client . ':' . $metric, wp_salt( 'nonce' ) );
		$bucket = hexdec( substr( $digest, 0, 4 ) ) % self::RATE_BUCKETS;
		$key    = self::rate_bucket_key( $bucket );
		$entry  = substr( $digest, 4, 16 );
		$now    = time();

		$counters = get_transient( $key );
		$counters = is_array( $counters ) ? $counters : array();
		foreach ( $counters as $identifier => $counter ) {
			if ( ! is_array( $counter ) || ! isset( $counter['expires'] ) || absint( $counter['expires'] ) <= $now ) {
				unset( $counters[ $identifier ] );
			}
		}

		$hits = isset( $counters[ $entry ]['hits'] ) ? absint( $counters[ $entry ]['hits'] ) : 0;
		if ( $hits >= self::RATE_LIMIT ) {
			return false;
		}

		$counters[ $entry ] = array(
			'hits'    => $hits + 1,
			'expires' => isset( $counters[ $entry ]['expires'] ) ? absint( $counters[ $entry ]['expires'] ) : $now + MINUTE_IN_SECONDS,
		);
		if ( count( $counters ) > self::RATE_BUCKET_ENTRIES ) {
			$counters = array_slice( $counters, -self::RATE_BUCKET_ENTRIES, null, true );
		}
		set_transient( $key, $counters, 2 * MINUTE_IN_SECONDS );
		return true;
	}

	/**
	 * Return the hourly display token for a given UTC hour.
	 *
	 * @param string $hour UTC hour in YmdH format.
	 * @return string
	 */
	private static function token( $hour ) {
		return hash_hmac( 'sha256', 'notification:' . $hour, wp_salt( 'nonce' ) );
	}

	/**
	 * Record a bounded aggregate interaction.
	 *
	 * @param \WP_REST_Request $request Request.
	 * @return \WP_REST_Response|\WP_Error
	 */
	public function interaction( $request ) {
		$metric = sanitize_key( $request->get_param( 'metric' ) );
		$client = sanitize_text_field( $request->get_param( 'client' ) );
		$token  = sanitize_text_field( $request->get_param( 'token' ) );
		$campaign = ( new NotificationSettings() )->get();
		$current  = $campaign ? self::token( gmdate( 'YmdH' ) ) : '';
		$previous = $campaign ? self::token( gmdate( 'YmdH', time() - HOUR_IN_SECONDS ) ) : '';
		$valid_token = $current && ( hash_equals( $current, $token ) || hash_equals( $previous, $token ) );
		if ( ! $valid_token || ! in_array( $metric, array( 'impressions', 'clicks', 'dismissals' ), true ) || ! preg_match( '/^[a-zA-Z0-9_-]{16,64}$/', $client ) ) {
			return new \WP_Error( 'voidpulse_invalid_interaction', __( 'Invalid interaction.', 'voidpulse-social-proof-for-woocommerce' ), array( 'status' => 400 ) );
		}
		if ( ! $this->within_rate_limit( $client, $metric ) ) {
			return new \WP_Error( 'voidpulse_rate_limited', __( 'Too many interactions.', 'voidpulse-social-proof-for-woocommerce' ), array( 'status' => 429 ) );
		}
		Analytics::increment( $metric );
		do_action( 'voidpulse_notification_' . ( 'impressions' === $metric ? 'displayed' : ( 'clicks' === $metric ? 'clicked' : 'dismissed' ) ), $metric );
		return rest_ensure_response( array( 'recorded' => true ) );
	}
}
