/*
 * Copyright 2023-2025 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.ai.vectorstore.hanadb;

import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.json.JsonMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.springframework.ai.document.Document;
import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.ai.model.EmbeddingUtils;
import org.springframework.ai.observation.conventions.VectorStoreProvider;
import org.springframework.ai.observation.conventions.VectorStoreSimilarityMetric;
import org.springframework.ai.util.JacksonUtils;
import org.springframework.ai.vectorstore.AbstractVectorStoreBuilder;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.observation.AbstractObservationVectorStore;
import org.springframework.ai.vectorstore.observation.VectorStoreObservationContext;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;

/**
 * The <b>SAP HANA Cloud vector engine</b> offers multiple use cases in AI scenarios.
 *
 * Recent advances in Generative AI (GenAI) and Large Language Models (LLM) have led to
 * increased awareness of and popularity for vector databases. Similarity search, a key
 * functionality of vector databases, complements traditional relational databases as well
 * as full-text search systems. Using natural language text as an example, embedding
 * functions map data to high dimensional vectors to preserve their semantic similarity.
 * Developers can then use vector-based semantic search to find similarity between
 * different passages of text. Because the data within an LLM is current only up to a
 * specific point in time, vector databases can offer additional relevant text to make
 * searches more accurate – known as <b>Retrieval Augmented Generation</b> (RAG).
 * Therefore, the addition of RAG to an LLM using a vector database like SAP HANA Cloud
 * provides an effective approach to increase the quality of responses from an LLM.
 *
 * The SAP HANA Cloud vector engine supports the create, read, update, and delete (CRUD)
 * operations involving vectors using SQL.
 *
 * <code>HanaCloudVectorStore</code> is an implementation of
 * <code>org.springframework.ai.vectorstore.VectorStore</code> interface that provides
 * implementation of <code>COSINE_SIMILARITY</code> function introduced in HanaDB in Mar,
 * 2024
 *
 * Hana DB introduced a new datatype <code>REAL_VECTOR</code> that can store embeddings
 * generated by <code>org.springframework.ai.embedding.EmbeddingModel</code>
 *
 * @author Rahul Mittal
 * @author Christian Tzolov
 * @author Sebastien Deleuze
 * @author Soby Chacko
 * @see <a href=
 * "https://help.sap.com/docs/hana-cloud-database/sap-hana-cloud-sap-hana-database-vector-engine-guide/introduction">SAP
 * HANA Database Vector Engine Guide</a>
 * @since 1.0.0
 */
public class HanaCloudVectorStore extends AbstractObservationVectorStore {

	private static final Logger logger = LoggerFactory.getLogger(HanaCloudVectorStore.class);

	private final HanaVectorRepository<? extends HanaVectorEntity> repository;

	private final String tableName;

	private final int topK;

	private final ObjectMapper objectMapper;

	/**
	 * Protected constructor that accepts a builder instance. This is the preferred way to
	 * create new HanaCloudVectorStore instances.
	 * @param builder the configured builder instance
	 */
	protected HanaCloudVectorStore(Builder builder) {
		super(builder);

		Assert.notNull(builder.repository, "Repository must not be null");

		this.repository = builder.repository;
		this.tableName = builder.tableName;
		this.topK = builder.topK;
		this.objectMapper = JsonMapper.builder().addModules(JacksonUtils.instantiateAvailableModules()).build();
	}

	/**
	 * Creates a new builder for configuring and creating HanaCloudVectorStore instances.
	 * @return a new builder instance
	 */
	public static Builder builder(HanaVectorRepository<? extends HanaVectorEntity> repository,
			EmbeddingModel embeddingModel) {
		return new Builder(repository, embeddingModel);
	}

	@Override
	public void doAdd(List<Document> documents) {
		int count = 1;
		for (Document document : documents) {
			logger.info("[{}/{}] Calling EmbeddingModel for document id = {}", count++, documents.size(),
					document.getId());
			String content = document.getText().replaceAll("\\s+", " ");
			String embedding = getEmbedding(document);
			this.repository.save(this.tableName, document.getId(), embedding, content);
		}
		logger.info("Embeddings saved in HanaCloudVectorStore for {} documents", count - 1);
	}

	@Override
	public void doDelete(List<String> idList) {
		int deleteCount = this.repository.deleteEmbeddingsById(this.tableName, idList);
		logger.info("{} embeddings deleted", deleteCount);
	}

	public int purgeEmbeddings() {
		int deleteCount = this.repository.deleteAllEmbeddings(this.tableName);
		logger.info("{} embeddings deleted", deleteCount);
		return deleteCount;
	}

	@Override
	public List<Document> similaritySearch(String query) {
		return similaritySearch(SearchRequest.builder().query(query).topK(this.topK).build());
	}

	@Override
	public List<Document> doSimilaritySearch(SearchRequest request) {
		if (request.hasFilterExpression()) {
			throw new UnsupportedOperationException(
					"SAPHanaVectorEngine does not support metadata filter expressions yet.");
		}

		String queryEmbedding = getEmbedding(request);
		List<? extends HanaVectorEntity> searchResult = this.repository.cosineSimilaritySearch(this.tableName,
				request.getTopK(), queryEmbedding);
		logger.info("Hana cosine-similarity for query={}, with topK={} returned {} results", request.getQuery(),
				request.getTopK(), searchResult.size());

		return searchResult.stream().map(c -> {
			try {
				return new Document(c.get_id(), this.objectMapper.writeValueAsString(c), Collections.emptyMap());
			}
			catch (JsonProcessingException e) {
				throw new RuntimeException(e);
			}
		}).collect(Collectors.toList());
	}

	private String getEmbedding(SearchRequest searchRequest) {
		return "[" + EmbeddingUtils.toList(this.embeddingModel.embed(searchRequest.getQuery()))
			.stream()
			.map(String::valueOf)
			.collect(Collectors.joining(", ")) + "]";
	}

	private String getEmbedding(Document document) {
		return "[" + EmbeddingUtils.toList(this.embeddingModel.embed(document))
			.stream()
			.map(String::valueOf)
			.collect(Collectors.joining(", ")) + "]";
	}

	@Override
	public VectorStoreObservationContext.Builder createObservationContextBuilder(String operationName) {

		return VectorStoreObservationContext.builder(VectorStoreProvider.HANA.value(), operationName)
			.dimensions(this.embeddingModel.dimensions())
			.collectionName(this.tableName)
			.similarityMetric(VectorStoreSimilarityMetric.COSINE.value());
	}

	/**
	 * Builder class for creating {@link HanaCloudVectorStore} instances.
	 * <p>
	 * Provides a fluent API for configuring all aspects of the HANA Cloud vector store.
	 *
	 * @since 1.0.0
	 */
	public static class Builder extends AbstractVectorStoreBuilder<Builder> {

		private final HanaVectorRepository<? extends HanaVectorEntity> repository;

		@Nullable
		private String tableName;

		private int topK;

		/**
		 * Sets the HANA vector repository.
		 * @param repository the repository to use
		 * @return the builder instance
		 * @throws IllegalArgumentException if repository is null
		 */
		private Builder(HanaVectorRepository<? extends HanaVectorEntity> repository, EmbeddingModel embeddingModel) {
			super(embeddingModel);
			Assert.notNull(repository, "Repository must not be null");
			this.repository = repository;
		}

		/**
		 * Sets the table name for vector storage.
		 * @param tableName the name of the table to use
		 * @return the builder instance
		 */
		public Builder tableName(String tableName) {
			this.tableName = tableName;
			return this;
		}

		/**
		 * Sets the number of top results to return.
		 * @param topK the number of results
		 * @return the builder instance
		 */
		public Builder topK(int topK) {
			this.topK = topK;
			return this;
		}

		@Override
		public HanaCloudVectorStore build() {
			return new HanaCloudVectorStore(this);
		}

	}

}
