package com.loremind.infrastructure.ai;

import com.loremind.domain.generationcontext.ChatMessage;
import com.loremind.domain.generationcontext.ChatRequest;
import com.loremind.domain.generationcontext.LoreStructuralContext;
import com.loremind.domain.generationcontext.LoreStructuralContext.FolderPage;
import com.loremind.domain.generationcontext.PageContext;
import com.loremind.domain.generationcontext.ports.AiChatProvider;
import com.loremind.domain.generationcontext.ports.AiProviderException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.MediaType;
import org.springframework.http.codec.ServerSentEvent;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Flux;

import java.time.Duration;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import java.util.stream.Collectors;

/**
 * Adapter de sortie : implémente AiChatProvider en appelant
 * le Brain Python via WebClient + SSE (Server-Sent Events).
 *
 * Responsabilités :
 *  1. Traduire ChatRequest (domaine) -> JSON attendu par /chat/stream.
 *  2. Consommer le flux SSE token par token.
 *  3. Invoquer onToken / onComplete / onError au bon moment.
 *  4. Traduire toute erreur technique en AiProviderException.
 *
 * Le domaine ne voit JAMAIS WebClient, Flux, ni la moindre URL.
 */
@Component
public class BrainAiChatClient implements AiChatProvider {

    private static final String CHAT_STREAM_PATH = "/chat/stream";
    private static final ParameterizedTypeReference<ServerSentEvent<String>> SSE_STRING_TYPE =
            new ParameterizedTypeReference<>() {};

    private final WebClient webClient;

    public BrainAiChatClient(
            WebClient.Builder builder,
            @Value("${brain.base-url}") String baseUrl) {
        this.webClient = builder.baseUrl(baseUrl).build();
    }

    @Override
    public void streamChat(
            ChatRequest request,
            Consumer<String> onToken,
            Runnable onComplete,
            Consumer<Throwable> onError) {

        Map<String, Object> payload = toPayload(request);

        Flux<ServerSentEvent<String>> flux = webClient.post()
                .uri(CHAT_STREAM_PATH)
                .contentType(MediaType.APPLICATION_JSON)
                .accept(MediaType.TEXT_EVENT_STREAM)
                .bodyValue(payload)
                .retrieve()
                .bodyToFlux(SSE_STRING_TYPE);

        try {
            // blockLast() : transforme le flux réactif en appel bloquant conforme
            // au contrat synchrone du port. L'appelant choisit le thread.
            flux
                .timeout(Duration.ofSeconds(120))
                .doOnNext(sse -> handleEvent(sse, onToken, onError))
                .blockLast();
            onComplete.run();
        } catch (Exception e) {
            onError.accept(new AiProviderException(
                    "Erreur lors du streaming chat depuis le Brain.", e));
        }
    }

    /** Dispatch selon le type d'événement SSE (data par défaut, done, error). */
    private void handleEvent(
            ServerSentEvent<String> sse,
            Consumer<String> onToken,
            Consumer<Throwable> onError) {
        String event = sse.event();  // null si pas d'event: xxx -> c'est un data par défaut
        String data = sse.data();

        if ("error".equals(event)) {
            onError.accept(new AiProviderException(
                    "Le Brain a signalé une erreur : " + data));
            return;
        }
        if ("done".equals(event)) {
            return; // la fin est gérée par blockLast + onComplete
        }
        // Défaut : événement data avec JSON {"token":"..."}.
        String token = extractToken(data);
        if (token != null && !token.isEmpty()) {
            onToken.accept(token);
        }
    }

    /**
     * Parse minimaliste du JSON {"token":"..."} sans pull Jackson ici.
     * Si le format se complexifie, on remplacera par un DTO Jackson.
     */
    private String extractToken(String json) {
        if (json == null) return null;
        int idx = json.indexOf("\"token\"");
        if (idx < 0) return null;
        int colon = json.indexOf(':', idx);
        int firstQuote = json.indexOf('"', colon + 1);
        int lastQuote = json.lastIndexOf('"');
        if (firstQuote < 0 || lastQuote <= firstQuote) return null;
        return json.substring(firstQuote + 1, lastQuote)
                .replace("\\n", "\n")
                .replace("\\\"", "\"")
                .replace("\\\\", "\\");
    }

    // --- Construction du payload JSON vers le Brain -------------------------

    private Map<String, Object> toPayload(ChatRequest request) {
        Map<String, Object> root = new LinkedHashMap<>();
        root.put("messages", request.getMessages().stream()
                .map(this::messageToMap)
                .collect(Collectors.toList()));
        root.put("lore_context", loreContextToMap(request.getLoreContext()));
        // page_context est optionnel côté Brain (Pydantic l'accepte null).
        // On ne l'ajoute au payload que s'il est effectivement fourni.
        if (request.getPageContext() != null) {
            root.put("page_context", pageContextToMap(request.getPageContext()));
        }
        return root;
    }

    private Map<String, Object> pageContextToMap(PageContext pc) {
        Map<String, Object> map = new LinkedHashMap<>();
        map.put("title", pc.getTitle());
        map.put("template_name", pc.getTemplateName());
        map.put("template_fields", pc.getTemplateFields());
        map.put("values", pc.getValues());
        return map;
    }

    private Map<String, Object> messageToMap(ChatMessage m) {
        Map<String, Object> map = new LinkedHashMap<>();
        map.put("role", m.getRole());
        map.put("content", m.getContent());
        return map;
    }

    private Map<String, Object> loreContextToMap(LoreStructuralContext ctx) {
        Map<String, Object> map = new LinkedHashMap<>();
        map.put("lore_name", ctx.getLoreName());
        map.put("lore_description", ctx.getLoreDescription());

        Map<String, Object> foldersMap = new LinkedHashMap<>();
        for (Map.Entry<String, List<FolderPage>> e : ctx.getFolders().entrySet()) {
            foldersMap.put(e.getKey(), e.getValue().stream()
                    .map(this::folderPageToMap)
                    .collect(Collectors.toList()));
        }
        map.put("folders", foldersMap);
        map.put("tags", ctx.getTags());
        return map;
    }

    private Map<String, Object> folderPageToMap(FolderPage fp) {
        Map<String, Object> map = new LinkedHashMap<>();
        map.put("title", fp.getTitle());
        map.put("template_name", fp.getTemplateName());
        return map;
    }
}
