<?php

namespace Limb_Chatbot\Includes\Services;

use Limb_Chatbot\Includes\Data_Objects\Vector;
use Limb_Chatbot\Includes\Data_Objects\Vector_Index;
use Limb_Chatbot\Includes\Data_Objects\Vector_Index_Vector;
use Limb_Chatbot\Includes\Exceptions\Error_Codes;
use Limb_Chatbot\Includes\Exceptions\Exception;
use Limb_Chatbot\Includes\Repositories\Vector_Repository;
use Limb_Chatbot\Includes\Utilities\Embedding_Utility;
use Limb_Chatbot\Includes\Utilities\Vector_Db_Utility;

/**
 * Class Vector_Service
 *
 * Handles creation, updating, syncing, deletion, and automatic management of vectors.
 *
 * @since 1.0.0
 */
class Vector_Service {
	/**
	 * Dataset meta key that stores allowed access roles.
	 */
	private const DATASET_META_USER_ROLES = 'user_roles';
	/**
	 * Special role allowing universal access.
	 */
	private const ROLE_EVERYONE = 'everyone';

	/**
	 * Vector repository instance.
	 *
	 * @var Vector_Repository
	 * @since 1.0.0
	 */
	public Vector_Repository $repository;

	/**
	 * Constructor.
	 *
	 * @param  Vector_Repository  $repository  Instance of vector repository for CRUD operations.
	 *
	 * @since 1.0.0
	 */
	public function __construct( Vector_Repository $repository ) {
		$this->repository      = $repository;
	}

	/**
	 * Creates a new vector record, generates its embedding.
	 *
	 * @param  array<string,mixed>  $data  Associative array of vector data to create.
	 *
	 * @return Vector The newly created and saved Vector instance.
	 *
	 * @throws \Exception
	 * @since 1.0.0
	 */
	public function create( array $data ) {
		$vector = Vector::make( $data );
		$this->assign_access_roles_from_dataset( $vector );
		$vector->refresh( ( new Embedding_Utility( $vector ) )->generate() );
		$vector->save();

		return $vector;
	}

	/**
	 * Updates an existing vector optionally with new data, regenerates embedding.
	 * Input is generating again, cause usually for existing vectors its null.
	 *
	 * @param  Vector  $vector  The Vector object to update.
	 * @param  array<string,mixed>|null  $data  Optional associative array to refresh vector data.
	 *
	 * @return Vector The updated Vector instance.
	 *
	 * @throws \Exception
	 * @since 1.0.0
	 */
	public function update( Vector $vector, $data = null ) {
		if ( ! empty( $data ) ) {
			$vector->refresh( Vector::make( $data ) );
		}
		$this->assign_access_roles_from_dataset( $vector );
		$generated_vector = ( new Embedding_Utility( $vector ) )->generate();
		$vector->refresh( $generated_vector );
		$vector->save();

		return $vector;
	}

	/**
	 * Inserts or updates a vector in a specified vector index.
	 *
	 * @param  Vector  $vector  The Vector instance to upsert.
	 * @param  Vector_Index  $vector_index  The target Vector_Index instance where the vector should be upserted.
	 *
	 * @return Vector_Index_Vector|null Returns the Vector_Index_Vector linking record on success, or null on failure.
	 *
	 * @throws \Exception
	 * @since 1.0.0
	 */
	public function upsert( Vector $vector, Vector_Index $vector_index ): ?Vector_Index_Vector {
		$vector_index_vectors = Vector_Index_Vector::where( [ 'vector_id' => $vector->get_id(), 'vector_index_id' => $vector_index->get_id() ] );
		if ( empty( $vector_index_vectors->first() ) ) {
			if ( $vector_index->get_config_id() ) {
				if ( ! ( new Vector_Db_Utility( $vector_index ) )->upsert( $vector ) ) {
					// Note that Pinecone will update their records in case of same vector upsert, but no info regarding other DBs like QDrant, etc ...
					throw new Exception( Error_Codes::TECHNICAL_ERROR, __( 'Failed to add Vector into storage.', 'limb-chatbot' ) );
				}
			}
			$vector_index_vector = Vector_Index_Vector::create( [ 'vector_id' => $vector->get_id(), 'vector_index_id' => $vector_index->get_id() ] );
		} else {
			$vector_index_vector = $vector_index_vectors->first();
		}

		return $vector_index_vector ?? null;
	}

	/**
	 * Inserts or updates multiple vectors in a vector index in batch.
	 *
	 * @param  Vector_Index  $vector_index  The Vector_Index where vectors will be upserted.
	 * @param  Vector[]  $vectors  Array of Vector instances to upsert.
	 *
	 * @return Vector_Index_Vector[] Array of Vector_Index_Vector linking records created.
	 *
	 * @throws Exception Throws on failure to upsert vectors to external vector DB.
	 *
	 * @since 1.0.0
	 */
	public function upsert_batch( Vector_Index $vector_index, array $vectors ): array {
		if ( $vector_index->get_config_id() ) {
			if ( ! ( new Vector_Db_Utility( $vector_index ) )->upsert_batch( $vectors ) ) {
				throw new Exception( Error_Codes::TECHNICAL_ERROR, __( 'Failed to upsert the vectors', 'limb-chatbot' ) );
			}
		}
		foreach ( $vectors as $vector ) {
			$vector_index_vectors[] = Vector_Index_Vector::create( [ 'vector_id' => $vector->get_id(), 'vector_index_id' => $vector_index->get_id() ] );
		}

		return $vector_index_vectors ?? [];
	}

	/**
	 * Deletes a vector including its external vector DB entry, linked vector index mappings, and metadata.
	 *
	 * @param  Vector  $vector  The Vector instance to delete.
	 *
	 * @return bool True on successful deletion; throws exception otherwise.
	 *
	 * @throws Exception Throws if failed to remove vector from external storage.
	 *
	 * @since 1.0.0
	 */
	public function delete( Vector $vector ): bool {
		$vector_index_vectors = Vector_Index_Vector::where( [ 'vector_id' => $vector->get_id() ] )->get() ?? [];
		foreach ( $vector_index_vectors as $vector_index_vector ) {
			if ( $vector_index = Vector_Index::find( $vector_index_vector->get_vector_index_id() ) ) {
				if ( $vector_index->get_config_id() ) {
					if ( ! ( new Vector_Db_Utility( $vector_index ) )->delete_vector( $vector ) ) {
						// translators: %s is the name of the external storage index that failed to be removed.
						throw new Exception( Error_Codes::TECHNICAL_ERROR, sprintf( __( 'Failed to remove from external storage %s', 'limb-chatbot' ), $vector_index->get_name() ) );
					}
				}
				Vector_Index_Vector::delete( [ 'vector_id' => $vector->get_id(), 'vector_index_id' => $vector_index->get_id() ] );
			}
		}
		Vector::delete( [ 'id' => $vector->get_id() ] );

		return true;
	}



	/**
	 * Syncs a vector instance with its external vector indexes.
	 *
	 * @param  Vector  $vector  The Vector to synchronize externally.
	 *
	 * @return bool True on success, false otherwise.
	 *
	 * @throws \Exception
	 * @since 1.0.0
	 */
	public function sync( Vector $vector ): bool {
		$vector = $this->update( $vector );
		foreach ( ( Vector_Index_Vector::where( [ 'vector_id' => $vector->get_id() ] )->get() ?? [] ) as $vector_index_vector ) {
			if ( $vector_index = Vector_Index::find( $vector_index_vector->get_vector_index_id() ) ) {
				if ( $vector_index->get_config_id() ) {
					$result = ( new Vector_Db_Utility( $vector_index ) )->upsert( $vector );
				}
			}
		}
		if ( ! isset( $result ) || $result ) {
			return true;
		}

		return false;
	}

	public function batch_delete( array $where ) {
		// Get vectors matching conditions, only 'id' and 'uuid'
		$vectors = Vector::where( $where, - 1, - 1, null, null, false, [ 'id', 'uuid' ] );
		// Exit if no vectors found
		if ( $vectors->is_empty() ) {
			return;
		}
		$vector_ids = array_values( $vectors->pluck( 'id' ) );
		// Get mappings to vector indexes
		$vector_index_vectors = Vector_Index_Vector::where( [ 'vector_id' => $vector_ids ], - 1, - 1 );
		$vector_index_ids     = $vector_index_vectors->pluck( 'vector_index_id' );
		if ( ! empty( $vector_index_ids ) ) {
			// Fetch vector indexes and delete vectors in batch
			$vector_index_ids = array_unique( $vector_index_ids );
			$vector_indexes   = Vector_Index::where( [ 'id' => $vector_index_ids ] );
			$vector_indexes->each( function ( Vector_Index $vector_index ) use ( $vectors ) {
				if ( $vector_index->get_config_id() ) {
					( new Vector_Db_Utility( $vector_index ) )->delete_vector_batch( $vectors->get() );
				}
			} );
		}
		// Delete mappings, metadata, and vectors
		Vector_Index_Vector::delete( [ 'vector_id' => $vector_ids ] );
		Vector::delete( [ 'id' => $vector_ids ] );
	}

	/**
	 * Refresh and persist access roles from related dataset metadata.
	 *
	 * @param  Vector  $vector
	 *
	 * @return Vector
	 */
	public function sync_access_roles( Vector $vector ): Vector {
		$this->assign_access_roles_from_dataset( $vector );
		$vector->save();

		return $vector;
	}

	/**
	 * Hydrate vector access roles from dataset metadata.
	 *
	 * @param  Vector  $vector
	 *
	 * @return void
	 */
	private function assign_access_roles_from_dataset( Vector $vector ): void {
		$dataset_entry = $vector->dataset_entry();
		if ( ! $dataset_entry ) {
			return;
		}

		$dataset = $dataset_entry->dataset();
		if ( ! $dataset ) {
			return;
		}

		$roles = $dataset->get_meta_value( self::DATASET_META_USER_ROLES );
		$roles = $this->normalize_access_roles( $roles );
		if ( empty( $roles ) ) {
			$roles = [ self::ROLE_EVERYONE ];
		}

		$vector->set_access_roles( $roles );
	}

	/**
	 * Normalize role payload into unique role slugs.
	 *
	 * @param  mixed  $roles
	 *
	 * @return string[]
	 */
	private function normalize_access_roles( $roles ): array {
		if ( null === $roles || false === $roles || '' === $roles ) {
			return [];
		}

		if ( is_string( $roles ) ) {
			$roles = str_contains( $roles, ',' ) ? array_map( 'trim', explode( ',', $roles ) ) : [ $roles ];
		}
		if ( ! is_array( $roles ) ) {
			return [];
		}

		return array_values( array_filter( array_unique( array_map( static function ( $role ) {
			return is_string( $role ) ? sanitize_key( $role ) : '';
		}, $roles ) ) ) );
	}
}