package **.****.****.llm;

import jakarta.enterprise.context.Dependent;
import jakarta.inject.Inject;
import jakarta.json.*;
import jakarta.json.stream.JsonGenerator;
import jakarta.ws.rs.client.Entity;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;

import java.io.Serializable;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.HashMap;
import java.util.Map;

/**
 * Classe pour gérer le JSON des requêtes à l'API de Gemini.
 * Contient l'état JSON de la conversation et des méthodes por manipuler le JSON.
 */
@Dependent
public class JsonUtilPourGemini implements Serializable {

    private String systemRole; // = "helpful assistant";
    /**
     * Pour ajouter une nouvelle valeur à la fin du tableau JSON "messages" dans le document JSON de la requête.
     * Le "-" final indique que la valeur sera ajoutée à la fin du tableau.
     */
    private final JsonPointer pointer = Json.createPointer(("/contents/-"));

    /**
     * Requête JSON, à partir du prompt de l'utilisateur.
     */
    private JsonObject requeteJson;
    private String texteRequeteJson;

    public void setSystemRole(String systemRole) {
        this.systemRole = systemRole;
    }

    /**
     * Pour envoyer une requête à l'API de Gemini.
     */
    @Inject
    private LlmClientPourGemini geminiClient;

    /**
     * Envoi une requête à l'API de Gemini.
     * Format du document JSON envoyé dans la requête vers l'API :
     * {
     *     "contents": [
     *         {
     *             "role": "user",
     *             "parts": [
     *                 {
     *                     "text": "Capitale de la France ?"
     *                 }
     *             ]
     *         },
     *         {
     *             "role": "model",
     *             "parts": [
     *                 {
     *                     "text": "Paris est la capitale de la France."
     *                 }
     *             ]
     *         },
     *         ...
     *     ]
     * }
     * * @param question question posée par l'utilisateur
     *
     * @return la réponse de l'API, sous la forme d'un texte simple (pas JSON).
     * @throws RequeteException exception lancée dans le cas où la requête a été rejetée par l'API.
     */
    public LlmInteraction envoyerRequete(String question) throws RequeteException {
        String requestBody;
        if (this.requeteJson == null) {
            // Si c'est la première question, crée la requête JSON avec le rôle système.
            requestBody = creerRequeteJson(this.systemRole, question);
        } else {
            // Ajout de la question.
            // Ce qui sera envoyé dans le corps de la requête POST.
            // Un message associé à la question doit être ajouté aux messages associés au début de la conversation.
            requestBody = ajouteQuestionDansJsonRequete(question);
        }

        Entity entity = Entity.entity(requestBody, MediaType.APPLICATION_JSON_TYPE);
        // Pour afficher la requête JSON dans la page JSF
        this.texteRequeteJson = prettyPrinting(requeteJson);
        // Envoi la requête par l'intermédiaire du client de l'API de Gemini.
        try (Response response = geminiClient.envoyerRequete(entity)) {
            // Entité incluse dans la réponse (texte au format JSON qui englobe la réponse à la question)
            String texteReponseJson = response.readEntity(String.class);
            if (response.getStatus() == 200) {
                return new LlmInteraction(this.texteRequeteJson, texteReponseJson, extractReponse(texteReponseJson));
            } else {
                // Pour voir la requête JSON s'il y a eu un problème.
                JsonObject objet = Json.createReader(new StringReader(requestBody)).readObject();
                throw new RequeteException(response.getStatus() + " : " + response.getStatusInfo(), prettyPrinting(objet));
            }
        }
    }

    /**
     * Crée une requête JSON pour envoyer à l'API de Gemini.
     * Il y a le rôle du système et la question de l'utilisateur.
     * Format du document JSON envoyé dans la requête vers l'API :
     * {
     *    "system_instruction": {
     *      "parts": [ {"text": "helpful assistant"} ]
     *    },
     *    "contents": [
     *        { "role": "user",
     *          "parts": [ { "text": "Capitale de la France ?" } ]
     *        }
     *    ]
     * }
     *
     * @param systemRole le rôle du système. Par exemple, "helpful assistant".
     * @param question question posée par l'utilisateur.
     * @return le texte du document JSON de la requête.
     */
    private String creerRequeteJson(String systemRole, String question) {
        // Création de l'objet "system_instruction"
        JsonArray systemInstructionParts = Json.createArrayBuilder()
                .add(Json.createObjectBuilder()
                        .add("text", systemRole))
                .build();
        JsonObject systemInstruction = Json.createObjectBuilder()
                .add("parts", systemInstructionParts)
                .build();
        // Création de l'objet "contents"
        JsonArray userContentParts = Json.createArrayBuilder()
                .add(Json.createObjectBuilder()
                        .add("text", question))
                .build();
        JsonObject userContent = Json.createObjectBuilder()
                .add("role", "user")
                .add("parts", userContentParts)
                .build();
        JsonArray contents = Json.createArrayBuilder()
                .add(userContent)
                .build();
        // Création de l'objet racine
        JsonObject rootJson = Json.createObjectBuilder()
                .add("system_instruction", systemInstruction)
                .add("contents", contents)
                .build();
        this.requeteJson = rootJson;

        return rootJson.toString();
    }

    /**
     * Modifie le JSON de la requete pour ajouter le JsonObject lié à la nouvelle question dans messagesJson.
     * Il faut ajouter au tableau JSON.
     *
     * @param nouvelleQuestion question posée par l'utilsateur.
     * @return le texte du document JSON de la requête.
     */
    private String ajouteQuestionDansJsonRequete(String nouvelleQuestion) {
        // Crée le nouveau JsonObject qui correspond à la nouvelle question
        JsonObject nouveauMessageJson = Json.createObjectBuilder()
                .add("text", nouvelleQuestion)
                .build();
        // Crée le JsonArray parts
        JsonObjectBuilder newPartBuilder = Json.createObjectBuilder()
                .add("role", "user")
                .add("parts", Json.createArrayBuilder()
                        .add(nouveauMessageJson)
                        .build());
        // Ajoute ce nouveau JsonObjet dans this.requeteJson
        this.requeteJson = this.pointer.add(this.requeteJson, newPartBuilder.build());
        // La requête sous la forme d'une String avec mise en forme (passage à la ligne et indentation).
        this.texteRequeteJson = prettyPrinting(requeteJson);
        return this.requeteJson.toString();
    }

    /**
     * Retourne le texte formaté du document JSON pour un affichage plus agréable.
     *
     * @param jsonObject l'objet JSON dont on veut une forme formatée.
     * @return la forme formatée
     */
    private String prettyPrinting(JsonObject jsonObject) {
        Map config = new HashMap<>();
        config.put(JsonGenerator.PRETTY_PRINTING, true);
        JsonWriterFactory writerFactory = Json.createWriterFactory(config);
        StringWriter stringWriter = new StringWriter();
        try (JsonWriter jsonWriter = writerFactory.createWriter(stringWriter)) {
            jsonWriter.write(jsonObject);
        }
        return stringWriter.toString();
    }

    /**
     * Extrait la réponse de l'API et ajoute la réponse à this.jsonRequete pour garder la conversation dans
     * la prochaine requête.
     * @param json le document JSON de la réponse.
     * @return juste la valeur de content qui contient la réponse à la question.
     */
    private String extractReponse(String json) {
        try (JsonReader jsonReader = Json.createReader(new StringReader(json))) {
            JsonObject jsonObject = jsonReader.readObject();
            JsonObject messageReponse = jsonObject
                    .getJsonArray("candidates")
                    .getJsonObject(0)
                    .getJsonObject("content");
            // Ajoute l'objet JSON de la réponse de l'API au JSON de la prochaine requête
            this.requeteJson = this.pointer.add(this.requeteJson, messageReponse);
            // Extrait seulement le texte de la réponse
            return messageReponse.getJsonArray("parts").getJsonObject(0).getString("text");
        }
    }

}