<?php

namespace RistrettoApps\Spawnster\Services;

use Spawnster;

/**
 * Provides methods for inserting and searching vectors in the database.
 *
 * @package    Spawnster
 * @subpackage Spawnster/Services
 * @author     Ristretto Apps
 */
class PostVectorTableService {

	/**
	 * Insert a vector into the vectors table and associate it with a post.
	 *
	 * @since    1.0.0
	 * @param    int   $post_id   The ID of the post to associate the vector with.
	 * @param    array $vector    The vector to insert.
	 * @return   int|WP_Error     The ID of the inserted vector or a WP_Error object on failure.
	 */
	public static function insert( int $post_id, array $vector ): int|WP_Error {
		global $wpdb;

		$magnitude              = VectorService::magnitude( $vector );
		$normalized_vector      = VectorService::normalize( $vector, $magnitude );
		$binary_code            = VectorService::vector_to_hex( $normalized_vector );
		$vector_table_name      = Spawnster::get_vector_table_name();
		$post_vector_table_name = Spawnster::get_post_vector_table_name();

		$vector            = wp_json_encode( $vector );
		$normalized_vector = wp_json_encode( $normalized_vector );

		$result = $wpdb->query(
			$wpdb->prepare(
				'INSERT INTO %i ( vector, normalized_vector, magnitude, binary_code ) VALUES ( %s, %s, %f, UNHEX(%s) )',
				$vector_table_name,
				$vector,
				$normalized_vector,
				$magnitude,
				$binary_code
			)
		);

		if ( false === $result ) {
			error_log( $wpdb->last_error );
			return new WP_Error( 'spawnster_insert_vector_error', 'Error inserting vector into vectors table.' );
		}

		$wpdb->insert(
			$post_vector_table_name,
			array(
				'post_id'   => $post_id,
				'vector_id' => $wpdb->insert_id,
			)
		);

		if ( false === $wpdb->insert_id ) {
			error_log( $wpdb->last_error );
			return new WP_Error( 'spawnster_insert_post_vector_error', 'Error inserting vector into post vectors table.' );
		}

		return $wpdb->insert_id;
	}

	/**
	 * Search for posts similar to the given vector.
	 *
	 * @since    1.0.0
	 * @param    array $vector    The vector to search for.
	 * @param    int   $n         The number of results to return.
	 * @return   array            An array of posts similar to the given vector.
	 */
	public static function search_posts( array $vector, int $n = 10 ): array {
		global $wpdb;

		$vector_table_name      = Spawnster::get_vector_table_name();
		$post_vector_table_name = Spawnster::get_post_vector_table_name();
		$vector                 = VectorService::normalize( $vector );
		$binary_code            = VectorService::vector_to_hex( $vector );

		// Initial search using binary codes.
		$results = $wpdb->get_results(
			$wpdb->prepare(
				'SELECT ID, BIT_COUNT(binary_code ^ UNHEX(%s)) AS hamming_distance FROM %i ORDER BY hamming_distance LIMIT %d',
				$binary_code,
				$vector_table_name,
				$n * 2 // Get more candidates for reranking
			),
			ARRAY_A
		);

		$candidates = array();
		foreach ( $results as $result ) {
			$candidates[] = $result['ID'];
		}

		if ( empty( $candidates ) ) {
			return array();
		}

		// Rerank candidates using cosine similarity and get post data.
		$sql_cosim_available = get_option( 'spawnster_sql_cosim_available' );
		$placeholders        = implode( ',', array_fill( 0, count( $candidates ), '%d' ) );
		$posts               = array();

		if ( ! $sql_cosim_available ) {
			$results = $wpdb->get_results(
				$wpdb->prepare(
					"SELECT p.ID, p.post_title, v.vector
					FROM {$wpdb->posts} p
					INNER JOIN %i pv ON p.ID = pv.post_id
					INNER JOIN %i v ON pv.vector_id = v.ID
					WHERE v.ID IN( $placeholders ) AND p.post_status = 'publish'
					ORDER BY p.ID DESC",
					$post_vector_table_name,
					$vector_table_name,
					...$candidates
				)
			);
			
			$similarities = array();

			foreach ( $results as $result ) {
				$stored_vector               = json_decode( $result->vector, true );
				$similarity                  = VectorService::cosine_similarity( $vector, $stored_vector );
				$similarities[ $result->ID ] = $similarity;
			}

			// Sort posts by highest similarity.
			array_multisort( $similarities, SORT_DESC, $results );

			$results = array_slice( $results, 0, $n );

			foreach ( $results as $result ) {
				$posts[] = array(
					'ID'         => (int) $result->ID,
					'title'      => $result->post_title,
					'similarity' => $similarities[ $result->ID ],
				);
			}
		} else {
			// Build the query parameters array properly
			$query_params = array(
				wp_json_encode( $vector ),
				$post_vector_table_name,
				$vector_table_name
			);
			// Add candidates to the parameters array
			$query_params = array_merge( $query_params, $candidates );
			// Add the limit
			$query_params[] = $n;
			
			$results = $wpdb->get_results(
				$wpdb->prepare(
					"SELECT p.ID, p.post_title, SPAWNSTER_COSIM( v.normalized_vector, %s ) as similarity
					FROM {$wpdb->posts} p
					INNER JOIN %i pv ON p.ID = pv.post_id
					INNER JOIN %i v ON pv.vector_id = v.ID
					WHERE v.ID IN( $placeholders ) AND p.post_status = 'publish'
					ORDER BY similarity DESC
					LIMIT %d",
					...$query_params
				)
			);

			foreach ( $results as $result ) {
				$posts[] = array(
					'ID'         => (int) $result->ID,
					'title'      => $result->post_title,
					'similarity' => (float) $result->similarity,
				);
			}
		}

		return $posts;
	}
} 