<?php
/**
 * Mock provider — deterministic vectors for CI without external APIs.
 *
 * Only registered when the `FINDASTRA_TEST_MODE` constant is true (typically
 * via wp-config.php on a test site). Never visible to buyers in production.
 *
 * Embedding scheme: pure hashed bag-of-tokens.
 *   - Each unique token in the input maps to a deterministic dimension via
 *     hash(token) mod (DIMS - 1) + 1. That dim is set to 1.0. Dim 0 stays
 *     reserved (unused — keeps a slot for future signaling, easy to spot in
 *     dumps).
 *   - L2-normalized so cosine === dot product (the engine's `dot()` is used
 *     verbatim, just like production providers).
 *   - When mock is the active provider, register() installs filters that
 *     drop the gating thresholds — bag-of-tokens cosines hover much lower
 *     than transformer embeddings (0.1–0.4 vs 0.6–0.7), so the production
 *     0.60 top-score gate would silently empty every search. Lowered to 0.05.
 *
 * Reproducibility: identical input always produces an identical vector. No
 * randomness, no time-dependence.
 *
 * @package FindAstra
 */

defined( 'ABSPATH' ) || exit;

final class FindAstra_Provider_Mock extends FindAstra_Provider_Base {

	public const KEY   = 'mock';
	public const MODEL = 'findastra/mock-bow-384';
	public const DIMS  = 384;

	public static function is_enabled(): bool {
		return defined( 'FINDASTRA_TEST_MODE' ) && true === FINDASTRA_TEST_MODE;
	}

	/**
	 * Wire up the lowered gating thresholds. Called on `plugins_loaded` by
	 * the bootstrap when the mock provider is enabled.
	 */
	public static function register_filters(): void {
		if ( ! self::is_enabled() ) {
			return;
		}
		$only_when_active = static function ( $default_value, $lowered ) {
			$opts = get_option( FindAstra_Admin::SETTINGS_OPTION, array() );
			$active = is_array( $opts ) && isset( $opts['provider'] ) ? (string) $opts['provider'] : '';
			return self::KEY === $active ? $lowered : $default_value;
		};
		add_filter( 'findastra_min_top_score', static fn( $v ) => $only_when_active( $v, 0.05 ) );
		add_filter( 'findastra_min_score',     static fn( $v ) => $only_when_active( $v, 0.0 ) );
		add_filter( 'findastra_score_window',  static fn( $v ) => $only_when_active( $v, 1.0 ) );
	}

	public function key(): string { return self::KEY; }
	public function label(): string { return __( 'Mock (testing)', 'findastra' ); }
	public function dims(): int { return self::DIMS; }
	public function model(): string { return self::MODEL; }
	public function requires_credentials(): bool { return false; }
	public function is_server_side(): bool { return true; }
	public function is_configured(): bool { return self::is_enabled(); }

	/**
	 * @param string[] $inputs
	 */
	public function embed( array $inputs ): array|WP_Error {
		if ( ! self::is_enabled() ) {
			return new WP_Error(
				'findastra_mock_disabled',
				__( 'Mock provider is not enabled.', 'findastra' ),
				array( 'status' => 503 )
			);
		}

		$out = array();
		foreach ( $inputs as $text ) {
			$out[] = self::vector_for( (string) $text );
		}
		return $out;
	}

	/**
	 * Pure-PHP, no WP deps — used by unit tests too.
	 *
	 * @return float[]
	 */
	public static function vector_for( string $text ): array {
		$vec = array_fill( 0, self::DIMS, 0.0 );

		$tokens = array_unique( self::tokenize( $text ) );
		foreach ( $tokens as $tok ) {
			$dim = self::hash_dim( $tok );
			$vec[ $dim ] = 1.0;
		}

		return self::l2_normalize( $vec );
	}

	/**
	 * @return string[]
	 */
	private static function tokenize( string $text ): array {
		$lower = function_exists( 'mb_strtolower' ) ? mb_strtolower( $text ) : strtolower( $text );
		$parts = preg_split( '/[^\p{L}\p{N}]+/u', $lower ) ?: array();
		return array_values( array_filter( $parts, static fn( $t ) => '' !== $t && mb_strlen( $t ) >= 2 ) );
	}

	private static function hash_dim( string $token ): int {
		// Reserve dim 0 for the bias; map tokens into 1..DIMS-1.
		$hex = substr( md5( $token ), 0, 8 );
		return ( hexdec( $hex ) % ( self::DIMS - 1 ) ) + 1;
	}

	/**
	 * @param float[] $vec
	 * @return float[]
	 */
	private static function l2_normalize( array $vec ): array {
		$sum_sq = 0.0;
		foreach ( $vec as $v ) {
			$sum_sq += $v * $v;
		}
		if ( $sum_sq <= 0.0 ) {
			return $vec;
		}
		$norm = sqrt( $sum_sq );
		foreach ( $vec as $i => $v ) {
			$vec[ $i ] = $v / $norm;
		}
		return $vec;
	}
}
