package com.loremind.infrastructure.ai;

import com.loremind.domain.generationcontext.CampaignStructuralContext;
import com.loremind.domain.generationcontext.CampaignStructuralContext.ArcSummary;
import com.loremind.domain.generationcontext.CampaignStructuralContext.BranchHint;
import com.loremind.domain.generationcontext.CampaignStructuralContext.ChapterSummary;
import com.loremind.domain.generationcontext.CampaignStructuralContext.SceneSummary;
import com.loremind.domain.generationcontext.ChatMessage;
import com.loremind.domain.generationcontext.ChatRequest;
import com.loremind.domain.generationcontext.LoreStructuralContext;
import com.loremind.domain.generationcontext.LoreStructuralContext.PageSummary;
import com.loremind.domain.generationcontext.NarrativeEntityContext;
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 (Architecture Hexagonale) : 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.
 *     Sérialise lore_context, page_context, campaign_context et
 *     narrative_entity de façon conditionnelle selon le scénario d'appel
 *     (chat Lore / chat Lore focalisé page / chat Campagne / chat Campagne
 *     focalisé arc-chapter-scene).
 *  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 -------------------------

    /**
     * Construit le payload JSON. Chaque contexte optionnel est omis s'il est
     * null, pour s'aligner sur le schéma Pydantic côté Brain (champs
     * Optional qui restent absents du dict transmis au LLM).
     */
    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()));

        if (request.getLoreContext() != null) {
            root.put("lore_context", loreContextToMap(request.getLoreContext()));
        }
        if (request.getPageContext() != null) {
            root.put("page_context", pageContextToMap(request.getPageContext()));
        }
        if (request.getCampaignContext() != null) {
            root.put("campaign_context", campaignContextToMap(request.getCampaignContext()));
        }
        if (request.getNarrativeEntity() != null) {
            root.put("narrative_entity", narrativeEntityToMap(request.getNarrativeEntity()));
        }
        return root;
    }

    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<PageSummary>> e : ctx.getFolders().entrySet()) {
            foldersMap.put(e.getKey(), e.getValue().stream()
                    .map(this::pageSummaryToMap)
                    .collect(Collectors.toList()));
        }
        map.put("folders", foldersMap);
        map.put("tags", ctx.getTags());
        return map;
    }

    private Map<String, Object> pageSummaryToMap(PageSummary ps) {
        Map<String, Object> map = new LinkedHashMap<>();
        map.put("title", ps.getTitle());
        map.put("template_name", ps.getTemplateName());
        // values/tags/related_page_titles ne sont sérialisés que s'ils contiennent
        // de l'info — payload réseau plus léger quand la page est vierge.
        if (ps.getValues() != null && !ps.getValues().isEmpty()) {
            map.put("values", ps.getValues());
        }
        if (ps.getTags() != null && !ps.getTags().isEmpty()) {
            map.put("tags", ps.getTags());
        }
        if (ps.getRelatedPageTitles() != null && !ps.getRelatedPageTitles().isEmpty()) {
            map.put("related_page_titles", ps.getRelatedPageTitles());
        }
        return map;
    }

    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> campaignContextToMap(CampaignStructuralContext ctx) {
        Map<String, Object> map = new LinkedHashMap<>();
        map.put("campaign_name", ctx.getCampaignName());
        map.put("campaign_description", ctx.getCampaignDescription());
        map.put("arcs", ctx.getArcs().stream()
                .map(this::arcSummaryToMap)
                .collect(Collectors.toList()));
        return map;
    }

    private Map<String, Object> arcSummaryToMap(ArcSummary a) {
        Map<String, Object> map = new LinkedHashMap<>();
        map.put("name", a.getName());
        map.put("description", a.getDescription());
        // Envoye au Python pour enrichir le prompt ("N illustrations attachees").
        // Serialise uniquement si > 0 pour economiser le payload sur les entites sans images.
        if (a.getIllustrationCount() > 0) {
            map.put("illustration_count", a.getIllustrationCount());
        }
        map.put("chapters", a.getChapters().stream()
                .map(this::chapterSummaryToMap)
                .collect(Collectors.toList()));
        return map;
    }

    private Map<String, Object> chapterSummaryToMap(ChapterSummary c) {
        Map<String, Object> map = new LinkedHashMap<>();
        map.put("name", c.getName());
        map.put("description", c.getDescription());
        if (c.getIllustrationCount() > 0) {
            map.put("illustration_count", c.getIllustrationCount());
        }
        map.put("scenes", c.getScenes().stream()
                .map(this::sceneSummaryToMap)
                .collect(Collectors.toList()));
        return map;
    }

    private Map<String, Object> sceneSummaryToMap(SceneSummary s) {
        Map<String, Object> map = new LinkedHashMap<>();
        map.put("name", s.getName());
        map.put("description", s.getDescription());
        if (s.getIllustrationCount() > 0) {
            map.put("illustration_count", s.getIllustrationCount());
        }
        // Branches narratives : serialise uniquement si presentes, pour garder
        // un payload leger sur les scenes lineaires classiques.
        if (s.getBranches() != null && !s.getBranches().isEmpty()) {
            map.put("branches", s.getBranches().stream()
                    .map(this::branchHintToMap)
                    .collect(Collectors.toList()));
        }
        return map;
    }

    private Map<String, Object> branchHintToMap(BranchHint b) {
        Map<String, Object> map = new LinkedHashMap<>();
        map.put("label", b.getLabel());
        map.put("target_scene_name", b.getTargetSceneName());
        if (b.getCondition() != null && !b.getCondition().isBlank()) {
            map.put("condition", b.getCondition());
        }
        return map;
    }

    private Map<String, Object> narrativeEntityToMap(NarrativeEntityContext ne) {
        Map<String, Object> map = new LinkedHashMap<>();
        map.put("entity_type", ne.getEntityType());
        map.put("title", ne.getTitle());
        map.put("fields", ne.getFields());
        return map;
    }
}
