<?php
/**
 * Request_Context — request-scoped capture of correlation_id, mcp_client, ip.
 *
 * Populated from the rest_pre_dispatch filter. Event_Logger reads from one
 * shared instance per request; there is no global state and no static cache.
 * Falls back to a server-side UUID for correlation_id when the caller does
 * not send X-Request-ID or X-MCP-Session.
 *
 * @package Ppok\AIToolkit
 * @license GPL-2.0-or-later
 */

namespace Ppok\AIToolkit;

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

final class Request_Context {

	private static ?self $instance = null;

	private ?string $correlation_id = null;
	private ?string $mcp_client     = null;
	private ?string $ip             = null;
	private bool $captured          = false;

	/**
	 * Process-wide accessor for the request-scoped instance.
	 *
	 * The Plugin orchestrator calls set_instance() once per request after
	 * wiring rest_pre_dispatch. Callers that need correlation_id without
	 * threading the Request_Context through their constructor (e.g. ability
	 * execute_callbacks invoked statically) read from here. Returns null
	 * in unit-test contexts where no Plugin has booted.
	 */
	public static function instance(): ?self {
		return self::$instance;
	}

	public static function set_instance( self $instance ): void {
		self::$instance = $instance;
	}

	public static function reset_instance(): void {
		self::$instance = null;
	}

	/**
	 * rest_pre_dispatch filter callback.
	 *
	 * Signature matches the filter; we never alter $result, only inspect $request.
	 *
	 * @param mixed            $result  Pass-through.
	 * @param \WP_REST_Server  $server  Unused.
	 * @param \WP_REST_Request $request Incoming REST request.
	 * @return mixed The original $result, unchanged.
	 */
	public function on_rest_pre_dispatch( $result, $server, $request ) {
		if ( $request instanceof \WP_REST_Request ) {
			$this->capture( $request );
		}
		return $result;
	}

	/**
	 * Populate this context from a REST request + $_SERVER fallbacks.
	 */
	public function capture( \WP_REST_Request $request ): void {
		$this->correlation_id = $this->derive_correlation_id( $request );
		$this->mcp_client     = $this->derive_mcp_client( $request );
		$this->ip             = $this->derive_ip();
		$this->captured       = true;
	}

	public function get_correlation_id(): string {
		if ( ! $this->captured ) {
			$this->capture_from_server();
		}
		return $this->correlation_id ?? '';
	}

	public function get_mcp_client(): ?string {
		if ( ! $this->captured ) {
			$this->capture_from_server();
		}
		return $this->mcp_client;
	}

	public function get_ip(): ?string {
		if ( ! $this->captured ) {
			$this->capture_from_server();
		}
		return $this->ip;
	}

	/**
	 * Fallback path for code that reads context outside a REST cycle.
	 */
	private function capture_from_server(): void {
		$this->correlation_id = $this->generate_uuid_v4();
		$ua                   = isset( $_SERVER['HTTP_USER_AGENT'] )
			? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) )
			: '';
		$this->mcp_client     = '' !== $ua ? $ua : null;
		$this->ip             = $this->derive_ip();
		$this->captured       = true;
	}

	private function derive_correlation_id( \WP_REST_Request $request ): string {
		foreach ( array( 'X-Request-ID', 'X-MCP-Session' ) as $header ) {
			$value = $request->get_header( $header );
			if ( is_string( $value ) && '' !== $value ) {
				return substr( $value, 0, 64 );
			}
		}
		return $this->generate_uuid_v4();
	}

	private function derive_mcp_client( \WP_REST_Request $request ): ?string {
		$ua = $request->get_header( 'User-Agent' );
		if ( ! is_string( $ua ) || '' === $ua ) {
			return null;
		}
		return substr( $ua, 0, 191 );
	}

	private function derive_ip(): ?string {
		if ( empty( $_SERVER['REMOTE_ADDR'] ) ) {
			return null;
		}
		$ip = sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) );
		return '' !== $ip ? substr( $ip, 0, 45 ) : null;
	}

	private function generate_uuid_v4(): string {
		if ( function_exists( 'wp_generate_uuid4' ) ) {
			return wp_generate_uuid4();
		}
		// wp_generate_uuid4() lives in wp-includes/formatting.php; this branch
		// only fires if the function file hasn't loaded yet during early hooks.
		$data    = random_bytes( 16 );
		$data[6] = chr( ( ord( $data[6] ) & 0x0F ) | 0x40 );
		$data[8] = chr( ( ord( $data[8] ) & 0x3F ) | 0x80 );
		return vsprintf( '%s%s-%s-%s-%s-%s%s%s', str_split( bin2hex( $data ), 4 ) );
	}
}
