package com.easyagents.embedding.openai;

import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.easyagents.core.document.Document;
import com.easyagents.core.model.client.HttpClient;
import com.easyagents.core.model.embedding.BaseEmbeddingModel;
import com.easyagents.core.model.embedding.EmbeddingOptions;
import com.easyagents.core.model.exception.ModelException;
import com.easyagents.core.store.VectorData;
import com.easyagents.core.util.JSONUtil;
import com.easyagents.core.util.Maps;
import com.easyagents.core.util.StringUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;

public class OpenAIEmbeddingModel extends BaseEmbeddingModel<OpenAIEmbeddingConfig> {

    private static final Logger LOG = LoggerFactory.getLogger(OpenAIEmbeddingModel.class);
    private static final String REDACTED_HEADER_VALUE = "[REDACTED]";

    private HttpClient httpClient = new HttpClient();

    public OpenAIEmbeddingModel(OpenAIEmbeddingConfig config) {
        super(config);
    }

    public HttpClient getHttpClient() {
        return httpClient;
    }

    public void setHttpClient(HttpClient httpClient) {
        this.httpClient = httpClient;
    }

    @Override
    public VectorData embed(Document document, EmbeddingOptions options) {
        Map<String, String> headers = new HashMap<>();
        headers.put("Content-Type", "application/json");
        headers.put("Authorization", "Bearer " + getConfig().getApiKey());

        String payload = promptToEmbeddingsPayload(document, options, config);
        String endpoint = config.getEndpoint();
        String requestUrl = endpoint + config.getRequestPath();
        // https://platform.openai.com/docs/api-reference/embeddings/create
        String response = httpClient.post(requestUrl, headers, payload);

        if (StringUtil.noText(response)) {
            logResponseParsingFailure(
                "response is null or empty",
                requestUrl,
                headers,
                payload,
                response
            );
            throw new ModelException("response is null or empty.");
        }

        try {
            JSONObject jsonObject = JSON.parseObject(response);
            String errorMessage = JSONUtil.detectErrorMessage(jsonObject);
            if (errorMessage != null) {
                logResponseParsingFailure(errorMessage, requestUrl, headers, payload, response);
                throw new ModelException(errorMessage);
            }

            VectorData vectorData = new VectorData();
            double[] embedding = JSONUtil.readDoubleArray(jsonObject, "$.data[0].embedding");
            if (embedding == null || embedding.length == 0) {
                String missingEmbeddingMessage = buildMissingEmbeddingMessage();
                logResponseParsingFailure(missingEmbeddingMessage, requestUrl, headers, payload, response);
                throw new ModelException(missingEmbeddingMessage);
            }
            vectorData.setVector(embedding);

            return vectorData;
        } catch (ModelException e) {
            throw e;
        } catch (RuntimeException e) {
            logResponseParsingFailure(e.getMessage(), requestUrl, headers, payload, response);
            throw new ModelException("Failed to parse embedding response.", e);
        }
    }


    /**
     * Builds the embeddings request payload for OpenAI-compatible providers.
     *
     * @param text document to embed
     * @param options embedding request options
     * @param config model configuration
     * @return JSON payload for the embeddings endpoint
     */
    public static String promptToEmbeddingsPayload(Document text, EmbeddingOptions options, OpenAIEmbeddingConfig config) {
        String model = options.getModelOrDefault(config.getModel());
        return Maps.of("model", model)
            .set("encoding_format", options.getEncodingFormatOrDefault("float"))
            .set("input", text.getContent())
            .setIfNotEmpty("user", options.getUser())
            .setIf(
                supportsDimensionsParameter(model) && options.getDimensions() != null,
                "dimensions",
                options.getDimensions()
            )
            .toJSON();
    }

    /**
     * Determines whether the upstream embeddings endpoint supports dynamic dimensions.
     *
     * @param model model name sent to the provider
     * @return true when dimensions should be sent
     */
    static boolean supportsDimensionsParameter(String model) {
        if (StringUtil.noText(model)) {
            return false;
        }
        String normalizedModel = model.toLowerCase(Locale.ROOT);
        return normalizedModel.contains("qwen3-embedding")
            || normalizedModel.contains("qwen");
    }

    /**
     * Builds a safe diagnostic message for malformed embeddings responses.
     *
     * @return diagnostic message without secrets or raw response content
     */
    private String buildMissingEmbeddingMessage() {
        return "Embedding response does not contain data[0].embedding."
            + " Please check provider, model, request path, dimensions, or response format."
            + " provider=" + config.getProvider()
            + ", model=" + config.getModel()
            + ", endpoint=" + config.getEndpoint()
            + ", requestPath=" + config.getRequestPath();
    }

    private void logResponseParsingFailure(String reason,
                                           String requestUrl,
                                           Map<String, String> requestHeaders,
                                           String requestBody,
                                           String responseBody) {
        LOG.error(
            "Embedding response parsing failed: reason={}\n"
                + "requestUrl={}\n"
                + "requestHeaders={}\n"
                + "requestBody={}\n"
                + "responseBody={}",
            reason,
            requestUrl,
            sanitizeHeadersForLogging(requestHeaders),
            requestBody,
            responseBody
        );
    }

    static Map<String, String> sanitizeHeadersForLogging(Map<String, String> headers) {
        Map<String, String> sanitizedHeaders = new LinkedHashMap<>();
        if (headers == null) {
            return sanitizedHeaders;
        }
        headers.forEach((name, value) -> sanitizedHeaders.put(
            name,
            "Authorization".equalsIgnoreCase(name) ? REDACTED_HEADER_VALUE : value
        ));
        return sanitizedHeaders;
    }
}
